update 3 modules

This commit is contained in:
ferdiansyah783 2025-08-02 02:33:10 +07:00
parent 68355e31a8
commit a40a1994e5
56 changed files with 6051 additions and 5234 deletions

3
.env
View File

@ -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/

25
package-lock.json generated
View File

@ -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",

View File

@ -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",

BIN
public/assets/img/_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -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 = () => {
/>
</span>
<span className="user-detail">
<span className="user-name">John Smilga</span>
<span className="user-role">Super Admin</span>
<span className="user-name">{authState.user?.name || 'User'}</span>
<span className="user-role">{authState.user?.role || 'Admin'}</span>
</span>
</span>
</Link>
@ -632,12 +641,12 @@ const Header = () => {
<span className="status online" />
</span>
<div className="profilesets">
<h6>John Smilga</h6>
<h5>Super Admin</h5>
<h6>{authState.user?.name || 'User'}</h6>
<h5>{authState.user?.role || 'Admin'}</h5>
</div>
</div>
<hr className="m-0" />
<Link className="dropdown-item" to={route.route}>
<Link className="dropdown-item" to={route.profile}>
<i className="me-2" data-feather="user" /> My Profile
</Link>
<Link className="dropdown-item" to={route.generalsettings}>
@ -645,14 +654,18 @@ const Header = () => {
Settings
</Link>
<hr className="m-0" />
<Link className="dropdown-item logout pb-0" to="/signin">
<button
className="dropdown-item logout pb-0"
onClick={handleLogout}
style={{ background: 'none', border: 'none', width: '100%', textAlign: 'left' }}
>
<ImageWithBasePath
src="assets/img/icons/log-out.svg"
alt="img"
className="me-2"
/>
Logout
</Link>
</button>
</div>
</div>
</li>
@ -675,9 +688,13 @@ const Header = () => {
<Link className="dropdown-item" to="generalsettings">
Settings
</Link>
<Link className="dropdown-item" to="signin">
<button
className="dropdown-item"
onClick={handleLogout}
style={{ background: 'none', border: 'none', width: '100%', textAlign: 'left' }}
>
Logout
</Link>
</button>
</div>
</div>
{/* /Mobile Menu */}

View File

@ -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 = () => {
</div>
);
console.log(publicRoutes, "dashboard");
return (
<div>
<Routes>
@ -58,7 +57,14 @@ const AllRoutes = () => {
<Route path={route.path} element={route.element} key={id} />
))}
</Route>
<Route path={"/"} element={<HeaderLayout />}>
<Route
path={"/"}
element={
<ProtectedRoute>
<HeaderLayout />
</ProtectedRoute>
}
>
{publicRoutes.map((route, id) => (
<Route path={route.path} element={route.element} key={id} />
))}

View File

@ -1404,6 +1404,13 @@ export const publicRoutes = [
element: <ProductDetail />,
route: Route,
},
{
id: 113.1,
path: `${routes.productdetails}/:id`,
name: "productdetails",
element: <ProductDetail />,
route: Route,
},
{
id: 114,
path: routes.warehouses,

View File

@ -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) => {
@ -81,190 +89,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)',
? "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',
? "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'
? 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 (
<div
key={`pagination-${themeKey}`}
className={`custom-pagination-container ${isDarkMode ? '' : 'light-mode'} ${className}`}
className={`custom-pagination-container ${
isDarkMode ? "" : "light-mode"
} ${className}`}
style={containerStyles}
>
{/* Pagination Info */}
{showInfo && (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: compact ? '8px' : '16px',
flexWrap: 'wrap',
gap: compact ? '8px' : '12px'
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: compact ? "8px" : "0px",
flexWrap: "wrap",
gap: compact ? "8px" : "12px",
}}
>
{showPageSizeSelector && (
<div style={{display: 'flex', alignItems: 'center', gap: compact ? '6px' : '12px'}}>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>Số hàng mỗi trang</span>
<div
style={{
display: "flex",
alignItems: "center",
gap: compact ? "6px" : "12px",
}}
>
<span
style={{
color: isDarkMode ? "#bdc3c7" : "#2c3e50",
fontSize: compact ? "12px" : "14px",
fontWeight: "500",
}}
>
Number of rows per page
</span>
<select
value={pageSize}
onChange={(e) => handlePageSizeClick(parseInt(e.target.value))}
disabled={loading}
style={{
background: loading
? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)')
: (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'),
? isDarkMode
? "linear-gradient(45deg, #7f8c8d, #95a5a6)"
: "linear-gradient(135deg, #f8f9fa, #e9ecef)"
: isDarkMode
? "linear-gradient(45deg, #34495e, #2c3e50)"
: "linear-gradient(135deg, #ffffff, #f8f9fa)",
border: isDarkMode
? '1px solid rgba(52, 152, 219, 0.3)'
: '1px solid #dee2e6',
borderRadius: compact ? '4px' : '6px',
color: isDarkMode ? '#ffffff' : '#495057',
padding: compact ? '2px 6px' : '4px 8px',
fontSize: compact ? '12px' : '14px',
cursor: loading ? 'not-allowed' : 'pointer',
? "1px solid rgba(52, 152, 219, 0.3)"
: "1px solid #dee2e6",
borderRadius: compact ? "4px" : "6px",
color: isDarkMode ? "#ffffff" : "#495057",
padding: compact ? "2px 6px" : "4px 8px",
fontSize: compact ? "12px" : "14px",
cursor: loading ? "not-allowed" : "pointer",
opacity: loading ? 0.7 : 1,
boxShadow: isDarkMode ? 'none' : (compact ? '0 1px 2px rgba(0, 0, 0, 0.05)' : '0 1px 3px rgba(0, 0, 0, 0.1)')
boxShadow: isDarkMode
? "none"
: compact
? "0 1px 2px rgba(0, 0, 0, 0.05)"
: "0 1px 3px rgba(0, 0, 0, 0.1)",
}}
>
{pageSizeOptions.map(option => (
{pageSizeOptions.map((option) => (
<option
key={option}
value={option}
style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}}
style={{
background: isDarkMode ? "#2c3e50" : "#ffffff",
color: isDarkMode ? "#ffffff" : "#495057",
}}
>
{option}
</option>
))}
</select>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>bản ghi</span>
</div>
)}
<div style={{display: 'flex', alignItems: 'center', gap: compact ? '6px' : '12px'}}>
<div
<span
style={{
background: isDarkMode
? 'linear-gradient(45deg, #3498db, #2ecc71)'
: 'linear-gradient(45deg, #007bff, #28a745)',
borderRadius: '50%',
width: compact ? '16px' : '24px',
height: compact ? '16px' : '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: compact ? '8px' : '12px',
boxShadow: isDarkMode
? (compact ? '0 1px 4px rgba(52, 152, 219, 0.3)' : '0 2px 8px rgba(52, 152, 219, 0.3)')
: (compact ? '0 1px 4px rgba(0, 123, 255, 0.2)' : '0 2px 8px rgba(0, 123, 255, 0.2)'),
transition: compact ? 'all 0.2s ease' : 'all 0.3s ease'
color: isDarkMode ? "#bdc3c7" : "#2c3e50",
fontSize: compact ? "12px" : "14px",
fontWeight: "500",
}}
>
📊
</div>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>
Xem <strong style={{color: isDarkMode ? '#3498db' : '#007bff'}}>{startRecord}</strong> đến <strong style={{color: isDarkMode ? '#3498db' : '#007bff'}}>{endRecord}</strong> của <strong style={{color: isDarkMode ? '#e74c3c' : '#dc3545'}}>{totalCount}</strong> bản
records
</span>
</div>
</div>
)}
{/* Pagination Buttons */}
<div
style={{
position: 'relative',
position: "relative",
zIndex: 1,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: compact ? '4px' : '8px'
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: compact ? "4px" : "8px",
}}
>
{Array.from({ length: totalPages }, (_, i) => {
const pageNum = i + 1;
const isActive = currentPage === pageNum;
return (
<button
key={pageNum}
onClick={() => handlePageClick(pageNum)}
disabled={loading}
style={getButtonStyles(isActive)}
onMouseEnter={(e) => {
if (!loading && !isActive) {
e.target.style.background = isDarkMode
? 'linear-gradient(45deg, #3498db, #2980b9)'
: 'linear-gradient(135deg, #e9ecef, #f8f9fa)';
e.target.style.transform = isDarkMode ? (compact ? 'scale(1.05)' : 'scale(1.1)') : (compact ? 'translateY(-1px) scale(1.02)' : 'translateY(-1px) scale(1.05)');
e.target.style.boxShadow = isDarkMode
? (compact ? '0 2px 6px rgba(52, 152, 219, 0.3)' : '0 4px 12px rgba(52, 152, 219, 0.4)')
: (compact ? '0 2px 4px rgba(0, 0, 0, 0.12)' : '0 3px 8px rgba(0, 0, 0, 0.15)');
e.target.style.borderColor = isDarkMode ? '#3498db' : '#adb5bd';
}
}}
onMouseLeave={(e) => {
if (!loading && !isActive) {
e.target.style.background = isDarkMode
? 'linear-gradient(45deg, #34495e, #2c3e50)'
: 'linear-gradient(135deg, #ffffff, #f8f9fa)';
e.target.style.transform = 'scale(1)';
e.target.style.boxShadow = 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)');
e.target.style.borderColor = isDarkMode ? 'rgba(52, 152, 219, 0.3)' : '#dee2e6';
}
}}
<nav aria-label="Custom pagination">
<ul className="pagination justify-content-center custom-pagination">
<li className="page-item">
<a
onClick={() => handlePageClick(1)}
className="page-link"
aria-label="First"
>
{pageNum}
</button>
);
})}
<DoubleLeftOutlined
style={{ fontSize: "12px", marginRight: "-10px" }}
/>
</a>
</li>
<li
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) handlePageClick(currentPage - 1);
}}
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
>
<a className="page-link">
<LeftOutlined style={{ fontSize: "12px" }} />
</a>
</li>
<li className="page-item active bg-primary text-secondary rounded-5">
<span className="page-link">{currentPage}</span>
</li>
<li
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages)
handlePageClick(currentPage + 1);
}}
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<a className="page-link" aria-label="Next">
<RightOutlined style={{ fontSize: "12px" }} />
</a>
</li>
<li className="page-item">
<a
onClick={() => handlePageClick(totalPages)}
className="page-link"
aria-label="Last"
>
<DoubleRightOutlined
style={{ fontSize: "12px", marginLeft: "-10px" }}
/>
</a>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
);
};

View File

@ -43,7 +43,7 @@
// Color variants
.primary {
--loader-color: #ff9f43;
--loader-color: #36175e;
--loader-secondary: rgba(255, 159, 67, 0.3);
}

View File

@ -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);

View File

@ -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 <Navigate to="/signin" replace />;
}
// If authenticated, render the protected component
return children;
};
export default ProtectedRoute;

View File

@ -60,22 +60,22 @@ export const SidebarData = [
submenuHdr: "Inventory",
submenuItems: [
{ label: "Tiến độ dự án", link: "/project-tracker",icon: <Icon.Layers />,showSubRoute: false},
{ label: "Sản phẩm", link: "/product-list", icon:<Icon.Box />,showSubRoute: false,submenu: false },
{ label: "Nhập kho", link: "/product-list-2", icon:<Icon.Package />,showSubRoute: false,submenu: false },
{ label: "Tồn kho", link: "/product-list-3", icon:<Icon.Archive />,showSubRoute: false,submenu: false },
{ label: "Project Progress", link: "/project-tracker",icon: <Icon.Layers />,showSubRoute: false},
{ label: "Products", link: "/product-list", icon:<Icon.Box />,showSubRoute: false,submenu: false },
{ label: "Stock In", link: "/product-list-2", icon:<Icon.Package />,showSubRoute: false,submenu: false },
{ label: "Stock On Hand", link: "/product-list-3", icon:<Icon.Archive />,showSubRoute: false,submenu: false },
{ label: "Create Product", link: "/add-product", icon: <Icon.PlusSquare />,showSubRoute: false, submenu: false },
{ label: "Sản phẩm hết hạn", link: "/expired-products", icon: <Icon.Codesandbox />,showSubRoute: false,submenu: false },
{ label: "Hàng tồn kho", link: "/low-stocks", icon: <Icon.TrendingDown />,showSubRoute: false,submenu: false },
{ label: "Danh mục", link: "/category-list", icon: <Icon.Codepen />,showSubRoute: false,submenu: false },
{ label: "Expired Products", link: "/expired-products", icon: <Icon.Codesandbox />,showSubRoute: false,submenu: false },
{ label: "Low Stock Items", link: "/low-stocks", icon: <Icon.TrendingDown />,showSubRoute: false,submenu: false },
{ label: "Category", link: "/category-list", icon: <Icon.Codepen />,showSubRoute: false,submenu: false },
{ label: "Sub Category", link: "/sub-categories", icon: <Icon.Speaker />,showSubRoute: false,submenu: false },
{ label: "Thương hiệu", link: "/brand-list", icon: <Icon.Tag />,showSubRoute: false,submenu: false },
{ label: "Brands", link: "/brand-list", icon: <Icon.Tag />,showSubRoute: false,submenu: false },
{ label: "Units", link: "/units", icon: <Icon.Speaker />,showSubRoute: false,submenu: false },
{ label: "Variant Attributes", link: "/variant-attributes", icon: <Icon.Layers />,showSubRoute: false,submenu: false },
{ label: "Bảo hành", link: "/warranty", icon: <Icon.Bookmark />,showSubRoute: false,submenu: false },
{ label: "Warranty", link: "/warranty", icon: <Icon.Bookmark />,showSubRoute: false,submenu: false },
{ label: "In Barcode", link: "/barcode", icon: <Icon.AlignJustify />, showSubRoute: false,submenu: false },
{ label: "In QR Code", link: "/qrcode", icon: <Icon.Maximize />,showSubRoute: false,submenu: false },
{ label: "Khách mời đám cưới", link: "/wedding-guest-list", icon: <Icon.Heart />,showSubRoute: false,submenu: false }
{ label: "Wedding Guests", link: "/wedding-guest-list", icon: <Icon.Heart />,showSubRoute: false,submenu: false }
]
},
{

View File

@ -1,7 +1,57 @@
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 = () => {
const dispatch = useDispatch();
const { creating } = useSelector((state) => state.categories);
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 (
<div>
{/* Add Category */}
@ -16,22 +66,33 @@ const AddCategoryList = () => {
</div>
<button
type="button"
className="close"
className="btn-close btn-close-danger"
data-bs-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
></button>
</div>
<div className="modal-body custom-modal-body">
<form>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Category</label>
<input type="text" className="form-control" />
<input
type="text"
className="form-control"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label">Category Slug</label>
<input type="text" className="form-control" />
<label className="form-label">Description</label>
<input
type="text"
className="form-control"
name="description"
value={formData.description}
onChange={handleInputChange}
/>
</div>
<div className="mb-0">
<div className="status-toggle modal-status d-flex justify-content-between align-items-center">
@ -48,14 +109,29 @@ const AddCategoryList = () => {
<div className="modal-footer-btn">
<button
type="button"
className="btn btn-cancel me-2"
className="btn btn-secondary me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<Link to="#" className="btn btn-submit">
Create Category
</Link>
<button
type="submit"
disabled={creating}
className="btn btn-submit"
>
{creating ? (
<>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Creating...
</>
) : (
"Create Category"
)}
</button>
</div>
</form>
</div>
@ -66,7 +142,7 @@ const AddCategoryList = () => {
</div>
{/* /Add Category */}
</div>
)
}
);
};
export default AddCategoryList
export default AddCategoryList;

View File

@ -1,7 +1,69 @@
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 = () => {
const dispatch = useDispatch();
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 (
<div>
{/* Edit Category */}
@ -16,29 +78,32 @@ const EditCategoryList = () => {
</div>
<button
type="button"
className="close"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
></button>
</div>
<div className="modal-body custom-modal-body">
<form>
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Category</label>
<input
type="text"
className="form-control"
defaultValue="Laptop"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label">Category Slug</label>
<label className="form-label">Description</label>
<input
type="text"
className="form-control"
defaultValue="laptop"
name="description"
value={formData.description}
onChange={handleInputChange}
/>
</div>
<div className="mb-0">
@ -56,14 +121,29 @@ const EditCategoryList = () => {
<div className="modal-footer-btn">
<button
type="button"
className="btn btn-cancel me-2"
className="btn btn-secondary me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<Link to="#" className="btn btn-submit">
Save Changes
</Link>
<button
type="submit"
disabled={updating}
className="btn btn-submit"
>
{updating ? (
<>
<span
className="spinner-border spinner-border-sm me-2"
role="status"
aria-hidden="true"
></span>
Updating...
</>
) : (
"Update Category"
)}
</button>
</div>
</form>
</div>
@ -74,7 +154,7 @@ const EditCategoryList = () => {
</div>
{/* /Edit Category */}
</div>
)
}
);
};
export default EditCategoryList
export default EditCategoryList;

View File

@ -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 (
<Table
key={props}
className="table datanew dataTable no-footer"
className="custom-table table datanew dataTable"
rowSelection={rowSelection}
columns={columns}
dataSource={dataSource}
rowKey={(record) => record.id}
pagination={{
pageSize: 100
}}
/>
);
};

View File

@ -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,
});

View File

@ -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,
});

View File

@ -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 {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View File

@ -5,4 +5,5 @@ const store = configureStore({
reducer: rootReducer,
});
export { store };
export default store;

File diff suppressed because it is too large Load Diff

View File

@ -1,50 +1,131 @@
import React, { useState } from 'react'
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import ImageWithBasePath from '../../core/img/imagewithbasebath';
import { Link } from 'react-router-dom';
import { ChevronUp, Filter, PlusCircle, RotateCcw, Sliders, StopCircle, Zap } from 'feather-icons-react/build/IconComponents';
import { useDispatch, useSelector } from 'react-redux';
import { setToogleHeader } from '../../core/redux/action';
import Select from 'react-select';
import { DatePicker } from 'antd';
import AddCategoryList from '../../core/modals/inventory/addcategorylist';
import EditCategoryList from '../../core/modals/inventory/editcategorylist';
import withReactContent from 'sweetalert2-react-content';
import Swal from 'sweetalert2';
import Table from '../../core/pagination/datatable'
import { Select, Tag } from "antd";
import {
ChevronUp,
PlusCircle,
RotateCcw,
Trash2,
} from "feather-icons-react/build/IconComponents";
import { useEffect, useState } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import CustomPagination from "../../components/CustomPagination";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import AddCategoryList from "../../core/modals/inventory/addcategorylist";
import EditCategoryList from "../../core/modals/inventory/editcategorylist";
import Table from "../../core/pagination/datatable";
import { setToogleHeader } from "../../core/redux/action";
import {
deleteCategory,
fetchCategories,
fetchCategory,
} from "../../core/redux/actions/categoryActions";
import { formatDate } from "../../utils/date";
const CategoryList = () => {
const {
categories: apiCategories,
loading,
error,
totalCategories,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.categories);
const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header);
const dataSource = useSelector((state) => state.categotylist_data);
const dataSource = apiCategories?.length > 0 ? apiCategories : [];
const [isFilterVisible, setIsFilterVisible] = useState(false);
const toggleFilterVisibility = () => {
setIsFilterVisible((prevVisibility) => !prevVisibility);
};
const [selectedDate, setSelectedDate] = useState(new Date());
const handleDateChange = (date) => {
setSelectedDate(date);
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadCategories = async () => {
try {
const searchParams = {
page: currentPage,
limit: pageSize,
search: debouncedSearchTerm || "",
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== "")
);
const oldandlatestvalue = [
{ value: 'date', label: 'Sort by Date' },
{ value: 'newest', label: 'Newest' },
{ value: 'oldest', label: 'Oldest' },
];
const category = [
{ value: 'chooseCategory', label: 'Choose Category' },
{ value: 'laptop', label: 'Laptop' },
{ value: 'electronics', label: 'Electronics' },
{ value: 'shoe', label: 'Shoe' },
];
const status = [
{ value: 'chooseStatus', label: 'Choose Status' },
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
];
await dispatch(fetchCategories(cleanParams));
} catch (error) {
console.error("Failed to load categories", error);
}
};
loadCategories();
}, [dispatch, currentPage, pageSize, debouncedSearchTerm]);
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
}, 500); // 500ms delay
return () => clearTimeout(timer);
}, [searchTerm]);
// Handle pagination
const handlePageChange = (page) => {
setCurrentPage(page);
};
// Handle page size change
const handlePageSizeChange = (newPageSize) => {
setPageSize(newPageSize);
setCurrentPage(1); // Reset to first page when changing page size
};
const handleSearch = (e) => {
const value = e.target.value;
setSearchTerm(value);
// Reset to first page when searching
setCurrentPage(1);
};
// Calculate pagination info
const totalRecords = totalCategories || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
const handleDeleteCategory = async (categoryId) => {
try {
await dispatch(deleteCategory(categoryId));
// Show success message
MySwal.fire({
title: "Deleted!",
text: "Category has been deleted successfully.",
icon: "success",
className: "btn btn-success",
customClass: {
confirmButton: "btn btn-success",
},
});
} catch (error) {
console.error("Failed to delete category:", error);
MySwal.fire({
title: "Error!",
text: "Failed to delete category. Please try again.",
icon: "error",
className: "btn btn-danger",
customClass: {
confirmButton: "btn btn-danger",
},
});
}
};
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
@ -70,82 +151,108 @@ const CategoryList = () => {
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
)
);
const dateOptions = [
{ label: "Sort By: Last 7 Days", value: "last7days" },
{ label: "Sort By: Last Month", value: "lastmonth" },
{ label: "Sort By: Ascending", value: "ascending" },
{ label: "Sort By: Descending", value: "descending" },
];
const columns = [
{
title: "Category",
dataIndex: "category",
render: (_, record) => {
return <span>{record.name}</span>;
},
sorter: (a, b) => a.category.length - b.category.length,
},
{
title: "Category Slug",
dataIndex: "categoryslug",
render: (_, record) => {
return <span>{record?.name?.toLowerCase()}</span>;
},
sorter: (a, b) => a.categoryslug.length - b.categoryslug.length,
},
{
title: "Created On",
dataIndex: "createdon",
render: (_, record) => {
return <span>{formatDate(record.created_at)}</span>;
},
sorter: (a, b) => a.createdon.length - b.createdon.length,
},
{
title: "Status",
dataIndex: "status",
render: (text) => (
<span className="badge badge-linesuccess">
<Link to="#"> {text}</Link>
</span>
),
render: () => <Tag color="#87d068">active</Tag>,
sorter: (a, b) => a.status.length - b.status.length,
},
{
title: 'Actions',
dataIndex: 'actions',
key: 'actions',
render: () => (
title: "Actions",
dataIndex: "actions",
key: "actions",
render: (_, record) => (
<td className="action-table-data">
<div className="edit-delete-action">
<Link className="me-2 p-2" to="#" data-bs-toggle="modal" data-bs-target="#edit-category">
<Link
className="me-2 p-2"
to="#"
data-bs-toggle="modal"
data-bs-target="#edit-category"
onClick={() => dispatch(fetchCategory(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<Link className="confirm-text p-2" to="#" >
<i data-feather="trash-2" className="feather-trash-2" onClick={showConfirmationAlert}></i>
<Link
className="confirm-text p-2"
to="#"
onClick={(e) => {
e.preventDefault();
MySwal.fire({
title: "Are you sure?",
text: "You won't be able to revert this!",
showCancelButton: true,
confirmButtonColor: "#00ff00",
confirmButtonText: "Yes, delete it!",
cancelButtonColor: "#ff0000",
cancelButtonText: "Cancel",
}).then((result) => {
if (result.isConfirmed) {
handleDeleteCategory(record.id || record.key);
}
});
}}
>
<Trash2 className="feather-trash-2" />
</Link>
</div>
</td>
)
),
},
]
];
const MySwal = withReactContent(Swal);
const showConfirmationAlert = () => {
MySwal.fire({
title: 'Are you sure?',
text: 'You won\'t be able to revert this!',
showCancelButton: true,
confirmButtonColor: '#00ff00',
confirmButtonText: 'Yes, delete it!',
cancelButtonColor: '#ff0000',
cancelButtonText: 'Cancel',
}).then((result) => {
if (result.isConfirmed) {
MySwal.fire({
title: 'Deleted!',
text: 'Your file has been deleted.',
className: "btn btn-success",
confirmButtonText: 'OK',
customClass: {
confirmButton: 'btn btn-success',
},
});
} else {
MySwal.close();
}
});
};
// const showConfirmationAlert = () => {
// MySwal.fire({
// title: "Are you sure?",
// text: "You won't be able to revert this!",
// showCancelButton: true,
// confirmButtonColor: "#00ff00",
// confirmButtonText: "Yes, delete it!",
// cancelButtonColor: "#ff0000",
// cancelButtonText: "Cancel",
// }).then((result) => {
// if (result.isConfirmed) {
// handleDeleteCategory();
// } else {
// MySwal.close();
// }
// });
// };
return (
<div>
<div className="page-wrapper">
@ -161,20 +268,25 @@ const CategoryList = () => {
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<ImageWithBasePath src="assets/img/icons/pdf.svg" alt="img" />
<ImageWithBasePath
src="assets/img/icons/pdf.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderExcelTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<ImageWithBasePath src="assets/img/icons/excel.svg" alt="img" />
<ImageWithBasePath
src="assets/img/icons/excel.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderPrinterTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<i data-feather="printer" className="feather-printer" />
</Link>
@ -182,7 +294,6 @@ const CategoryList = () => {
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
@ -190,13 +301,14 @@ const CategoryList = () => {
</li>
<li>
<OverlayTrigger placement="top" overlay={renderCollapseTooltip}>
<Link
data-bs-toggle="tooltip"
data-bs-placement="top"
id="collapse-header"
className={data ? "active" : ""}
onClick={() => { dispatch(setToogleHeader(!data)) }}
onClick={() => {
dispatch(setToogleHeader(!data));
}}
>
<ChevronUp />
</Link>
@ -225,100 +337,69 @@ const CategoryList = () => {
type="text"
placeholder="Search"
className="form-control form-control-sm formsearch"
onChange={handleSearch}
/>
<Link to className="btn btn-searchset">
<i data-feather="search" className="feather-search" />
</Link>
</div>
</div>
<div className="search-path">
<Link className={`btn btn-filter ${isFilterVisible ? "setclose" : ""}`} id="filter_search">
<Filter
className="filter-icon"
onClick={toggleFilterVisibility}
/>
<span onClick={toggleFilterVisibility}>
<ImageWithBasePath src="assets/img/icons/closes.svg" alt="img" />
</span>
</Link>
</div>
<div className="form-sort">
<Sliders className="info-img" />
<Select
className="select"
options={oldandlatestvalue}
placeholder="Newest"
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
</div>
{/* /Filter */}
<div
className={`card${isFilterVisible ? " visible" : ""}`}
id="filter_inputs"
style={{ display: isFilterVisible ? "block" : "none" }}
>
<div className="card-body pb-0">
<div className="row">
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<Zap className="info-img" />
<Select
options={category}
className="select"
placeholder="Choose Category"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<i data-feather="calendar" className="info-img" />
<div className="input-groupicon">
<DatePicker
selected={selectedDate}
onChange={handleDateChange}
type="date"
className="filterdatepicker"
dateFormat="dd-MM-yyyy"
placeholder='Choose Date'
/>
</div>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<StopCircle className="info-img" />
<Select options={status} className="select" placeholder="Choose Status" />
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12 ms-auto">
<div className="input-blocks">
<Link className="btn btn-filters ms-auto">
{" "}
<i data-feather="search" className="feather-search" />{" "}
Search{" "}
</Link>
</div>
</div>
</div>
</div>
</div>
{/* /Filter */}
<div className="table-responsive">
{loading ? (
<div className="text-center p-4">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-2">Loading categories...</p>
</div>
) : error ? (
<div className="alert alert-danger" role="alert">
<strong>Error:</strong> {error}
<button
className="btn btn-sm btn-outline-danger ms-2"
onClick={() => dispatch(fetchCategories())}
>
Retry
</button>
</div>
) : (
<>
<Table columns={columns} dataSource={dataSource} />
<CustomPagination
currentPage={currentPage}
pageSize={pageSize}
totalCount={totalRecords}
totalPages={actualTotalPages}
loading={loading}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
pageSizeOptions={[10, 20, 50, 100]}
showInfo={true}
showPageSizeSelector={true}
compact={false}
className="product-list-pagination"
/>
</>
)}
</div>
</div>
</div>
{/* /product list */}
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
)
}
);
};
export default CategoryList
export default CategoryList;

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,29 @@
import React from 'react'
import ImageWithBasePath from '../../core/img/imagewithbasebath'
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import productsApi from "../../services/productsApi";
import { formatRupiah } from "../../utils/currency";
const ProductDetail = () => {
const { id } = useParams();
const [currentProduct, setCurrentProduct] = useState({});
useEffect(() => {
const fetchProduct = async () => {
try {
const response = await productsApi.getProductById(id);
setCurrentProduct(response.data);
} catch (error) {
console.error("Error fetching product:", error);
}
};
if (id) {
fetchProduct();
}
}, [id]);
return (
<div>
<div className="page-wrapper">
@ -18,28 +40,30 @@ const ProductDetail = () => {
<div className="card">
<div className="card-body">
<div className="bar-code-view">
<ImageWithBasePath src="assets/img/barcode/barcode1.png" alt="barcode" />
<ImageWithBasePath
src="assets/img/barcode/barcode1.png"
alt="barcode"
/>
<a className="printimg">
<ImageWithBasePath src="assets/img/icons/printer.svg" alt="print" />
<ImageWithBasePath
src="assets/img/icons/printer.svg"
alt="print"
/>
</a>
</div>
<div className="productdetails">
<ul className="product-bar">
<li>
<h4>Product</h4>
<h6>Macbook pro </h6>
<h6>{currentProduct?.name} </h6>
</li>
<li>
<h4>Category</h4>
<h6>Computers</h6>
<h6>{currentProduct?.category ?? "-"}</h6>
</li>
<li>
<h4>Sub Category</h4>
<h6>None</h6>
</li>
<li>
<h4>Brand</h4>
<h6>None</h6>
<h4>Business</h4>
<h6>{currentProduct?.business_type ?? "-"}</h6>
</li>
<li>
<h4>Unit</h4>
@ -47,56 +71,82 @@ const ProductDetail = () => {
</li>
<li>
<h4>SKU</h4>
<h6>PT0001</h6>
</li>
<li>
<h4>Minimum Qty</h4>
<h6>5</h6>
</li>
<li>
<h4>Quantity</h4>
<h6>50</h6>
</li>
<li>
<h4>Tax</h4>
<h6>0.00 %</h6>
</li>
<li>
<h4>Discount Type</h4>
<h6>Percentage</h6>
<h6>{currentProduct?.sku ?? "-"}</h6>
</li>
<li>
<h4>Price</h4>
<h6>1500.00</h6>
<h6>{formatRupiah(Number(currentProduct?.price))}</h6>
</li>
<li>
<h4>Status</h4>
<h6>Active</h6>
<h6>
{currentProduct?.is_active ? (
<span className="badge text-bg-success">
Active
</span>
) : (
"Inactive"
)}
</h6>
</li>
<li>
<h4>Description</h4>
<h6>
Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industrys
standard dummy text ever since the 1500s,
</h6>
<h6>{currentProduct?.description ?? "-"}</h6>
</li>
</ul>
</div>
</div>
</div>
{Array.isArray(currentProduct?.variants) && (
<div className="page-header">
<div className="page-title">
<h4>Variant Product Details</h4>
<h6>Full details of a variant product</h6>
</div>
</div>
)}
{Array.isArray(currentProduct?.variants) &&
currentProduct.variants.map((variant, index) => (
<div key={index} className="card">
<div className="card-body">
<div className="productdetails">
<ul className="product-bar">
<li>
<h4>Variant</h4>
<h6>{variant?.name} </h6>
</li>
<li>
<h4>Price</h4>
<h6>
{formatRupiah(Number(variant.price_modifier))}
</h6>
</li>
<li>
<h4>Cost</h4>
<h6>{formatRupiah(Number(variant.cost))}</h6>
</li>
</ul>
</div>
</div>
</div>
))}
</div>
<div className="col-lg-4 col-sm-12">
<div className="card">
<div className="card-body">
<div className="slider-product-details">
<div className="owl-carousel owl-theme product-slide">
<div className="slider-product">
<ImageWithBasePath src="assets/img/products/product69.jpg" alt="img" />
<h4>macbookpro.jpg</h4>
<ImageWithBasePath
src="assets/img/products/product69.jpg"
alt="img"
/>
<h4>{currentProduct?.name}</h4>
<h6>581kb</h6>
</div>
</div>
</div>
</div>
@ -106,10 +156,8 @@ const ProductDetail = () => {
{/* /add */}
</div>
</div>
</div>
)
}
);
};
export default ProductDetail
export default ProductDetail;

View File

@ -1,36 +1,33 @@
import { Select, Space } from "antd";
import {
Box,
ChevronUp,
Edit,
Eye,
Filter,
GitMerge,
PlusCircle,
RotateCcw,
Sliders,
StopCircle,
Trash2,
} from "feather-icons-react/build/IconComponents";
import React, { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { Download } from "react-feather";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import Select from "react-select";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import CustomPagination from "../../components/CustomPagination";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import Brand from "../../core/modals/inventory/brand";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { all_routes } from "../../Router/all_routes";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import Table from "../../core/pagination/datatable";
import { setToogleHeader } from "../../core/redux/action";
import { Download } from "react-feather";
import {
fetchProducts,
fetchProduct,
clearProductError,
deleteProduct,
clearProductError
fetchProduct,
fetchProducts,
} from "../../core/redux/actions/productActions";
import CustomPagination from '../../components/CustomPagination';
import { all_routes } from "../../Router/all_routes";
import categoriesApi from "../../services/categoriesApi";
import { formatRupiah } from "../../utils/currency";
// Add CSS animations for beautiful UI
const shimmerKeyframes = `
@ -62,11 +59,16 @@ const shimmerKeyframes = `
`;
// Inject CSS into head if not already present
if (typeof document !== 'undefined' && !document.getElementById('beautiful-pagination-styles')) {
const styleSheet = document.createElement('style');
styleSheet.id = 'beautiful-pagination-styles';
styleSheet.type = 'text/css';
styleSheet.innerText = shimmerKeyframes + `
if (
typeof document !== "undefined" &&
!document.getElementById("beautiful-pagination-styles")
) {
const styleSheet = document.createElement("style");
styleSheet.id = "beautiful-pagination-styles";
styleSheet.type = "text/css";
styleSheet.innerText =
shimmerKeyframes +
`
/* Hide all Ant Design pagination elements */
.ant-pagination,
.ant-pagination-item,
@ -433,40 +435,24 @@ const ProductList = () => {
totalProducts,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage
currentPage: reduxCurrentPage,
} = useSelector((state) => state.products);
// Fallback to legacy data if API data is not available
const legacyProducts = useSelector((state) => state.legacy?.product_list || []);
const dataSource = apiProducts.length > 0 ? apiProducts : legacyProducts;
const dataSource = apiProducts?.length > 0 ? apiProducts : [];
const dispatch = useDispatch();
const data = useSelector((state) => state.legacy?.toggle_header || false);
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
// State for pagination - sync with Redux
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 20);
// State for filter values
const [filterValues, setFilterValues] = useState({
product: '',
category: '',
subCategory: '',
brand: '',
priceRange: ''
});
const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
const [category, setCategory] = useState(null);
// Debounced search term
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const toggleFilterVisibility = () => {
setIsFilterVisible((prevVisibility) => !prevVisibility);
};
const [categoryOptions, setCategoryOptions] = useState([]);
const route = all_routes;
@ -479,29 +465,51 @@ const ProductList = () => {
return () => clearTimeout(timer);
}, [searchTerm]);
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,
}));
formattedCategory.unshift({ value: "", label: "All" });
setCategoryOptions(formattedCategory);
} catch (error) {
console.error("Failed to fetch categories", error);
}
};
loadCategories();
}, []);
// Fetch products when debounced search term or pagination changes
useEffect(() => {
const loadProducts = async () => {
try {
const searchParams = {
Page: currentPage,
PageSize: pageSize,
SearchTerm: debouncedSearchTerm || ''
page: currentPage,
limit: pageSize,
search: debouncedSearchTerm || "",
category_id: category,
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== '')
Object.entries(searchParams).filter(([, value]) => value !== "")
);
await dispatch(fetchProducts(cleanParams));
} catch (error) {
console.error('Failed to load products:', error);
console.error("Failed to load products:", error);
}
};
loadProducts();
}, [dispatch, currentPage, pageSize, debouncedSearchTerm]);
}, [dispatch, currentPage, pageSize, debouncedSearchTerm, category]);
// Handle product deletion
const handleDeleteProduct = async (productId) => {
@ -518,7 +526,7 @@ const ProductList = () => {
},
});
} catch (error) {
console.error('Failed to delete product:', error);
console.error("Failed to delete product:", error);
MySwal.fire({
title: "Error!",
text: "Failed to delete product. Please try again.",
@ -542,74 +550,12 @@ const ProductList = () => {
// Handle pagination
const handlePageChange = (page) => {
setCurrentPage(page);
// Dispatch action to fetch products for the new page
const searchParams = {
Page: page,
PageSize: pageSize,
SearchTerm: debouncedSearchTerm || ''
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== '')
);
dispatch(fetchProducts(cleanParams));
};
// Handle page size change
const handlePageSizeChange = (newPageSize) => {
setPageSize(newPageSize);
setCurrentPage(1); // Reset to first page when changing page size
// Dispatch action to fetch products with new page size
const searchParams = {
Page: 1,
PageSize: newPageSize,
SearchTerm: debouncedSearchTerm || ''
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== '')
);
dispatch(fetchProducts(cleanParams));
};
// Handle filter value changes
const handleFilterChange = (filterType, value) => {
setFilterValues(prev => ({
...prev,
[filterType]: value
}));
};
// Handle search with filters
const handleSearchWithFilters = () => {
setCurrentPage(1); // Reset to first page when searching
// Combine search term with filter values
const searchParams = {
Page: 1,
PageSize: pageSize,
SearchTerm: debouncedSearchTerm || '',
// Map filter values to API expected parameters
ProductName: filterValues.product || '',
Category: filterValues.category || '',
SubCategory: filterValues.subCategory || '',
Brand: filterValues.brand || '',
PriceRange: filterValues.priceRange || ''
};
// Remove empty parameters to clean up API call
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== '')
);
console.log('Search with filters (clean params):', cleanParams);
dispatch(fetchProducts(cleanParams));
};
// Calculate pagination info
@ -617,85 +563,73 @@ const ProductList = () => {
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
// Debug logs removed for production
// Clear error when component unmounts
useEffect(() => {
return () => {
dispatch(clearProductError());
};
}, [dispatch]);
const options = [
{ value: "sortByDate", label: "Sort by Date" },
{ value: "140923", label: "14 09 23" },
{ value: "110923", label: "11 09 23" },
{ label: "Sort By: Last 7 Days", value: "last7days" },
{ label: "Sort By: Last Month", value: "lastmonth" },
{ label: "Sort By: Ascending", value: "ascending" },
{ label: "Sort By: Descending", value: "descending" },
];
// Removed unused select option arrays since we're using simple inputs now
const columns = [
{
title: "Sản phẩm",
dataIndex: "product",
render: (text, record) => (
<span className="productimgname">
<Link to="/profile" className="product-img stock-img">
<ImageWithBasePath
alt={record.name || text || "Product"}
src={record.productImage || record.image || record.img}
/>
</Link>
<Link to="/profile">{text}</Link>
</span>
),
sorter: (a, b) => a.product.length - b.product.length,
},
{
title: "Mã",
title: "SKU",
dataIndex: "sku",
render: (_, record) => {
const sku = record.sku || record.code || record.productCode || '-';
const sku = record.sku || record.code || record.productCode || "-";
return <span>{sku}</span>;
},
sorter: (a, b) => {
const skuA = a.sku || a.code || a.productCode || '';
const skuB = b.sku || b.code || b.productCode || '';
const skuA = a.sku || a.code || a.productCode || "";
const skuB = b.sku || b.code || b.productCode || "";
return skuA.length - skuB.length;
},
},
{
title: "Danh mục",
title: "Product",
dataIndex: "product",
render: (text, record) => <span className="fw-medium">{record.name}</span>,
sorter: (a, b) => a.product.length - b.product.length,
},
{
title: "Category",
dataIndex: "category",
render: (_, record) => {
const category = record.category || record.categoryName || '-';
const category = record.category || record.categoryName || "-";
return <span>{category}</span>;
},
sorter: (a, b) => {
const catA = a.category || a.categoryName || '';
const catB = b.category || b.categoryName || '';
const catA = a.category || a.categoryName || "";
const catB = b.category || b.categoryName || "";
return catA.length - catB.length;
},
},
{
title: "Thương hiệu",
dataIndex: "brand",
title: "Business",
dataIndex: "business",
render: (_, record) => {
const brand = record.brand || record.brandName || '-';
const brand = record.business_type || record.brandName || "-";
return <span>{brand}</span>;
},
sorter: (a, b) => {
const brandA = a.brand || a.brandName || '';
const brandB = b.brand || b.brandName || '';
const brandA = a.brand || a.brandName || "";
const brandB = b.brand || b.brandName || "";
return brandA.length - brandB.length;
},
},
{
title: "Giá",
title: "Price",
dataIndex: "price",
render: (_, record) => {
const price = record.price || record.salePrice || record.unitPrice || 0;
return <span>${Number(price).toFixed(2)}</span>;
return <span>{formatRupiah(Number(price))}</span>;
},
sorter: (a, b) => {
const priceA = Number(a.price || a.salePrice || a.unitPrice || 0);
@ -704,15 +638,15 @@ const ProductList = () => {
},
},
{
title: "Đơn vị",
title: "Unit",
dataIndex: "unit",
render: (_, record) => {
const unit = record.unit || record.unitOfMeasure || '-';
const unit = record.unit || record.unitOfMeasure || "-";
return <span>{unit}</span>;
},
sorter: (a, b) => {
const unitA = a.unit || a.unitOfMeasure || '';
const unitB = b.unit || b.unitOfMeasure || '';
const unitA = a.unit || a.unitOfMeasure || "";
const unitB = b.unit || b.unitOfMeasure || "";
return unitA.length - unitB.length;
},
},
@ -721,7 +655,12 @@ const ProductList = () => {
dataIndex: "qty",
render: (_, record) => {
// Try multiple possible field names for quantity
const quantity = record.qty || record.quantity || record.stock || record.stockQuantity || 0;
const quantity =
record.qty ||
record.quantity ||
record.stock ||
record.stockQuantity ||
0;
return <span>{quantity}</span>;
},
sorter: (a, b) => {
@ -732,11 +671,14 @@ const ProductList = () => {
},
{
title: "Người tạo",
title: "Created By",
dataIndex: "createdby",
render: (text, record) => (
<span className="created-by-text">
<Link to="/profile" style={{ color: '#3498db', textDecoration: 'none' }}>
<Link
to="/profile"
style={{ color: "#3498db", textDecoration: "none" }}
>
{record.createdBy || text || "Admin"}
</Link>
</span>
@ -744,14 +686,17 @@ const ProductList = () => {
sorter: (a, b) => a.createdby.length - b.createdby.length,
},
{
title: "Thao tác",
title: "Action",
dataIndex: "action",
render: (text, record) => (
<td className="action-table-data">
<div className="edit-delete-action">
<div className="input-block add-lists"></div>
<Link className="me-2 p-2" to={route.productdetails}>
<Eye className="feather-view" />
<Link
className="me-2 p-2"
to={`${route.productdetails}/${record.id || record.key}`}
>
<Eye className="feather-edit" />
</Link>
<Link
className="me-2 p-2"
@ -795,8 +740,6 @@ const ProductList = () => {
];
const MySwal = withReactContent(Swal);
// Removed showConfirmationAlert as we handle confirmation inline
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
@ -822,14 +765,15 @@ const ProductList = () => {
Collapse
</Tooltip>
);
return (
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Danh sách sản phẩm</h4>
<h6>Quản sản phẩm</h6>
<h4>Product List</h4>
<h6>Manage your products</h6>
</div>
</div>
<ul className="table-top-head">
@ -884,18 +828,17 @@ const ProductList = () => {
<div className="page-btn">
<Link to={route.addproduct} className="btn btn-added">
<PlusCircle className="me-2 iconsize" />
Thêm mới
Add New Product
</Link>
</div>
<div className="page-btn import">
<Link
to="#"
className="btn btn-added color"
className="btn btn-outline-primary rounded-2"
data-bs-toggle="modal"
data-bs-target="#view-notes"
>
<Download className="me-2" />
Nhập sản phẩm
<Download className="me-2 iconsize" />
Import Product
</Link>
</div>
</div>
@ -917,291 +860,28 @@ const ProductList = () => {
</Link>
</div>
</div>
<div className="search-path">
<Link
className={`btn btn-filter ${
isFilterVisible ? "setclose" : ""
}`}
id="filter_search"
>
<Filter
className="filter-icon"
onClick={toggleFilterVisibility}
/>
<span onClick={toggleFilterVisibility}>
<ImageWithBasePath
src="assets/img/icons/closes.svg"
alt="img"
/>
</span>
</Link>
</div>
<div className="form-sort">
<Sliders className="info-img" />
<Space warp>
<Select
className="select"
style={{ height: 36, width: 100 }}
placeholder={"Category"}
options={categoryOptions}
value={
categoryOptions.find(
(option) => option.value === category
) || null
}
onChange={(selectedOption) => setCategory(selectedOption)}
/>
<Select
style={{ height: 36 }}
defaultValue={options[0]?.value}
options={options}
placeholder="14 09 23"
/>
</Space>
</div>
</div>
{/* /Filter */}
<div
className={`card${isFilterVisible ? " visible" : ""}`}
id="filter_inputs"
style={{ display: isFilterVisible ? "block" : "none" }}
>
<div className="card-body pb-0">
<div className="row">
<div className="col-lg-12 col-sm-12">
<div className="row">
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks custom-dropdown">
<select
className="form-control custom-select"
value={filterValues.product}
onChange={(e) => handleFilterChange('product', e.target.value)}
style={{
paddingLeft: '40px',
background: '#2c3e50',
border: '1px solid rgba(52, 152, 219, 0.3)',
color: '#ffffff',
borderRadius: '6px',
height: '40px',
appearance: 'none',
backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23ffffff\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '16px',
paddingRight: '40px'
}}
>
<option value="">Choose Product</option>
<option value="lenovo">Lenovo 3rd Generation</option>
<option value="nike">Nike Jordan</option>
<option value="apple">Apple iPhone</option>
<option value="samsung">Samsung Galaxy</option>
</select>
<Box
className="info-img"
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: '#3498db',
zIndex: 2,
pointerEvents: 'none'
}}
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks custom-dropdown">
<select
className="form-control custom-select"
value={filterValues.category}
onChange={(e) => handleFilterChange('category', e.target.value)}
style={{
paddingLeft: '40px',
background: '#2c3e50',
border: '1px solid rgba(52, 152, 219, 0.3)',
color: '#ffffff',
borderRadius: '6px',
height: '40px',
appearance: 'none',
backgroundImage: 'url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23ffffff\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3e%3cpolyline points=\'6,9 12,15 18,9\'%3e%3c/polyline%3e%3c/svg%3e")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '16px',
paddingRight: '40px'
}}
>
<option value="">Choose Category</option>
<option value="laptop">Laptop</option>
<option value="phone">Phone</option>
<option value="shoe">Shoe</option>
<option value="clothing">Clothing</option>
</select>
<StopCircle
className="info-img"
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: '#e74c3c',
zIndex: 2,
pointerEvents: 'none'
}}
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks custom-dropdown">
<select
className="form-control custom-select"
value={filterValues.subCategory}
onChange={(e) => handleFilterChange('subCategory', e.target.value)}
style={{
paddingLeft: '40px',
background: '#2c3e50',
border: '1px solid rgba(52, 152, 219, 0.3)',
color: '#ffffff',
borderRadius: '6px',
height: '40px',
appearance: 'none',
backgroundImage: 'url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' fill=\'white\' viewBox=\'0 0 16 16\'%3e%3cpath d=\'M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\'/%3e%3c/svg%3e")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '12px',
paddingRight: '40px'
}}
>
<option value="">Choose Sub Category</option>
<option value="computers">Computers</option>
<option value="accessories">Accessories</option>
<option value="sports">Sports</option>
<option value="electronics">Electronics</option>
</select>
<GitMerge
className="info-img"
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: '#2ecc71',
zIndex: 2,
pointerEvents: 'none'
}}
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks custom-dropdown">
<select
className="form-control custom-select"
value={filterValues.brand}
onChange={(e) => handleFilterChange('brand', e.target.value)}
style={{
paddingLeft: '40px',
background: '#2c3e50',
border: '1px solid rgba(52, 152, 219, 0.3)',
color: '#ffffff',
borderRadius: '6px',
height: '40px',
appearance: 'none',
backgroundImage: 'url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' fill=\'white\' viewBox=\'0 0 16 16\'%3e%3cpath d=\'M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\'/%3e%3c/svg%3e")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '12px',
paddingRight: '40px'
}}
>
<option value="">Choose Brand</option>
<option value="lenovo">Lenovo</option>
<option value="nike">Nike</option>
<option value="apple">Apple</option>
<option value="samsung">Samsung</option>
<option value="adidas">Adidas</option>
</select>
<StopCircle
className="info-img"
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: '#f39c12',
zIndex: 2,
pointerEvents: 'none'
}}
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks custom-dropdown">
<select
className="form-control custom-select"
value={filterValues.priceRange}
onChange={(e) => handleFilterChange('priceRange', e.target.value)}
style={{
paddingLeft: '40px',
background: '#2c3e50',
border: '1px solid rgba(52, 152, 219, 0.3)',
color: '#ffffff',
borderRadius: '6px',
height: '40px',
appearance: 'none',
backgroundImage: 'url("data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' fill=\'white\' viewBox=\'0 0 16 16\'%3e%3cpath d=\'M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\'/%3e%3c/svg%3e")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right 12px center',
backgroundSize: '12px',
paddingRight: '40px'
}}
>
<option value="">Choose Price Range</option>
<option value="0-100">$0 - $100</option>
<option value="100-500">$100 - $500</option>
<option value="500-1000">$500 - $1,000</option>
<option value="1000+">$1,000+</option>
</select>
<i
className="fas fa-money-bill info-img"
style={{
position: 'absolute',
left: '12px',
top: '50%',
transform: 'translateY(-50%)',
color: '#9b59b6',
zIndex: 2,
pointerEvents: 'none'
}}
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<button
className="btn btn-filters ms-auto"
onClick={handleSearchWithFilters}
type="button"
style={{
background: 'linear-gradient(45deg, #3498db, #2980b9)',
border: '1px solid rgba(52, 152, 219, 0.3)',
color: '#ffffff',
borderRadius: '6px',
padding: '8px 16px',
cursor: 'pointer',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.target.style.background = 'linear-gradient(45deg, #2980b9, #3498db)';
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
}}
onMouseLeave={(e) => {
e.target.style.background = 'linear-gradient(45deg, #3498db, #2980b9)';
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = 'none';
}}
>
<i
data-feather="search"
className="feather-search"
style={{ marginRight: '8px' }}
/>
Search
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* /Filter */}
<div className="table-responsive">
{loading ? (
<div className="text-center p-4">

View File

@ -1,16 +1,47 @@
import React from "react";
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { Link, useNavigate } from "react-router-dom";
import ImageWithBasePath from "../../../core/img/imagewithbasebath";
import { Link } from "react-router-dom";
import { all_routes } from "../../../Router/all_routes";
import authApi from "../../../services/authApi";
const Signin = () => {
const route = all_routes;
const navigate = useNavigate();
// const dispatch = useDispatch();
const authState = useSelector((state) => state.auth);
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [error, setError] = useState('');
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
try {
await authApi.login(formData);
navigate(route.dashboard);
} catch (error) {
setError(error.message || 'Login failed');
}
};
return (
<div className="main-wrapper">
<div className="account-content">
<div className="login-wrapper bg-img">
<div className="login-content">
<form action="index">
<form onSubmit={handleSubmit}>
<div className="login-userset">
<div className="login-logo logo-normal">
<ImageWithBasePath src="assets/img/logo.png" alt="img" />
@ -24,10 +55,22 @@ const Signin = () => {
Access the Dreamspos panel using your email and passcode.
</h4>
</div>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<div className="form-login mb-3">
<label className="form-label">Email Address</label>
<div className="form-addons">
<input type="text" className="form- control" />
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className="form-control"
required
/>
<ImageWithBasePath
src="assets/img/icons/mail.svg"
alt="img"
@ -39,7 +82,11 @@ const Signin = () => {
<div className="pass-group">
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
className="pass-input form-control"
required
/>
<span className="fas toggle-password fa-eye-slash" />
</div>
@ -63,9 +110,13 @@ const Signin = () => {
</div>
</div>
<div className="form-login">
<Link to={route.dashboard} className="btn btn-login">
Sign In
</Link>
<button
type="submit"
className="btn btn-login"
disabled={authState.loading}
>
{authState.loading ? 'Signing In...' : 'Sign In'}
</button>
</div>
<div className="signinform">
<h4>

View File

@ -1,8 +1,11 @@
import React from "react";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
const Profile = () => {
const { user } = useSelector((state) => state.auth);
return (
<div className="page-wrapper">
<div className="content">
@ -28,16 +31,18 @@ const Profile = () => {
<div className="profileupload">
<input type="file" id="imgInp" />
<Link to="#">
<ImageWithBasePath src="assets/img/icons/edit-set.svg" alt="img" />
<ImageWithBasePath
src="assets/img/icons/edit-set.svg"
alt="img"
/>
</Link>
</div>
</div>
<div className="profile-contentname">
<h2>William Castillo</h2>
<h2>{user?.name}</h2>
<h4>Updates Your Photo and Personal Details.</h4>
</div>
</div>
</div>
</div>
<div className="row">
@ -68,6 +73,7 @@ const Profile = () => {
type="email"
className="form-control"
defaultValue="william@example.com"
value={user?.email}
/>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,71 @@
import React, { useState } from 'react'
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import ImageWithBasePath from '../../core/img/imagewithbasebath';
import { ChevronUp, RotateCcw } from 'feather-icons-react/build/IconComponents';
import { setToogleHeader } from '../../core/redux/action';
import { useDispatch, useSelector } from 'react-redux';
import { Filter, PlusCircle, Sliders, StopCircle, User, Zap } from 'react-feather';
import Select from 'react-select';
import withReactContent from 'sweetalert2-react-content';
import Swal from 'sweetalert2';
import Table from '../../core/pagination/datatable'
import AddUsers from '../../core/modals/usermanagement/addusers';
import EditUser from '../../core/modals/usermanagement/edituser';
import React, { useEffect, useState } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { Link } from "react-router-dom";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import { ChevronUp, RotateCcw } from "feather-icons-react/build/IconComponents";
import { setToogleHeader } from "../../core/redux/action";
import { useDispatch, useSelector } from "react-redux";
import {
Filter,
PlusCircle,
Sliders,
StopCircle,
User,
Zap,
} from "react-feather";
import Select from "react-select";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Table from "../../core/pagination/datatable";
import AddUsers from "../../core/modals/usermanagement/addusers";
import EditUser from "../../core/modals/usermanagement/edituser";
import usersApi from "../../services/usersApi";
const Users = () => {
const oldandlatestvalue = [
{ value: 'date', label: 'Sort by Date' },
{ value: 'newest', label: 'Newest' },
{ value: 'oldest', label: 'Oldest' },
{ value: "date", label: "Sort by Date" },
{ value: "newest", label: "Newest" },
{ value: "oldest", label: "Oldest" },
];
const users = [
{ value: 'Choose Name', label: 'Choose Name' },
{ value: 'Lilly', label: 'Lilly' },
{ value: 'Benjamin', label: 'Benjamin' },
{ value: "Choose Name", label: "Choose Name" },
{ value: "Lilly", label: "Lilly" },
{ value: "Benjamin", label: "Benjamin" },
];
const status = [
{ value: 'Choose Name', label: 'Choose Status' },
{ value: 'Active', label: 'Active' },
{ value: 'InActive', label: 'InActive' },
{ value: "Choose Name", label: "Choose Status" },
{ value: "Active", label: "Active" },
{ value: "InActive", label: "InActive" },
];
const role = [
{ value: 'Choose Role', label: 'Choose Role' },
{ value: 'AcStore Keeper', label: 'Store Keeper' },
{ value: 'Salesman', label: 'Salesman' },
{ value: "Choose Role", label: "Choose Role" },
{ value: "AcStore Keeper", label: "Store Keeper" },
{ value: "Salesman", label: "Salesman" },
];
const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header);
const dataSource = useSelector((state) => state.userlist_data);
const [dataSource, setDataSource] = useState([])
const [isFilterVisible, setIsFilterVisible] = useState(false);
const toggleFilterVisibility = () => {
setIsFilterVisible((prevVisibility) => !prevVisibility);
};
useEffect(() => {
const loadUsers = async () => {
try {
const response = await usersApi.getAllUsers();
setDataSource(response)
console.log('response', response)
} catch (error) {
console.error("Failed to fetch users", error);
}
};
loadUsers();
}, []);
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
@ -70,10 +90,9 @@ const Users = () => {
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
)
);
const columns = [
{
title: "User Name",
dataIndex: "username",
@ -121,60 +140,68 @@ const Users = () => {
{text === "Inactive" && (
<span className="badge badge-linedanger">{text}</span>
)}
</div>
),
sorter: (a, b) => a.status.length - b.status.length,
},
{
title: 'Actions',
dataIndex: 'actions',
key: 'actions',
title: "Actions",
dataIndex: "actions",
key: "actions",
render: () => (
<td className="action-table-data">
<div className="edit-delete-action">
<Link className="me-2 p-2" to="#">
<i data-feather="eye" className="feather feather-eye action-eye"></i>
<i
data-feather="eye"
className="feather feather-eye action-eye"
></i>
</Link>
<Link className="me-2 p-2" to="#" data-bs-toggle="modal" data-bs-target="#edit-units">
<Link
className="me-2 p-2"
to="#"
data-bs-toggle="modal"
data-bs-target="#edit-units"
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<Link className="confirm-text p-2" to="#" >
<i data-feather="trash-2" className="feather-trash-2" onClick={showConfirmationAlert}></i>
<Link className="confirm-text p-2" to="#">
<i
data-feather="trash-2"
className="feather-trash-2"
onClick={showConfirmationAlert}
></i>
</Link>
</div>
</td>
)
),
},
]
];
const MySwal = withReactContent(Swal);
const showConfirmationAlert = () => {
MySwal.fire({
title: 'Are you sure?',
text: 'You won\'t be able to revert this!',
title: "Are you sure?",
text: "You won't be able to revert this!",
showCancelButton: true,
confirmButtonColor: '#00ff00',
confirmButtonText: 'Yes, delete it!',
cancelButtonColor: '#ff0000',
cancelButtonText: 'Cancel',
confirmButtonColor: "#00ff00",
confirmButtonText: "Yes, delete it!",
cancelButtonColor: "#ff0000",
cancelButtonText: "Cancel",
}).then((result) => {
if (result.isConfirmed) {
MySwal.fire({
title: 'Deleted!',
text: 'Your file has been deleted.',
title: "Deleted!",
text: "Your file has been deleted.",
className: "btn btn-success",
confirmButtonText: 'OK',
confirmButtonText: "OK",
customClass: {
confirmButton: 'btn btn-success',
confirmButton: "btn btn-success",
},
});
} else {
MySwal.close();
}
});
};
return (
@ -192,20 +219,25 @@ const Users = () => {
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<ImageWithBasePath src="assets/img/icons/pdf.svg" alt="img" />
<ImageWithBasePath
src="assets/img/icons/pdf.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderExcelTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<ImageWithBasePath src="assets/img/icons/excel.svg" alt="img" />
<ImageWithBasePath
src="assets/img/icons/excel.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderPrinterTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<i data-feather="printer" className="feather-printer" />
</Link>
@ -213,7 +245,6 @@ const Users = () => {
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
@ -221,13 +252,14 @@ const Users = () => {
</li>
<li>
<OverlayTrigger placement="top" overlay={renderCollapseTooltip}>
<Link
data-bs-toggle="tooltip"
data-bs-placement="top"
id="collapse-header"
className={data ? "active" : ""}
onClick={() => { dispatch(setToogleHeader(!data)) }}
onClick={() => {
dispatch(setToogleHeader(!data));
}}
>
<ChevronUp />
</Link>
@ -263,13 +295,21 @@ const Users = () => {
</div>
</div>
<div className="search-path">
<Link className={`btn btn-filter ${isFilterVisible ? "setclose" : ""}`} id="filter_search">
<Link
className={`btn btn-filter ${
isFilterVisible ? "setclose" : ""
}`}
id="filter_search"
>
<Filter
className="filter-icon"
onClick={toggleFilterVisibility}
/>
<span onClick={toggleFilterVisibility}>
<ImageWithBasePath src="assets/img/icons/closes.svg" alt="img" />
<ImageWithBasePath
src="assets/img/icons/closes.svg"
alt="img"
/>
</span>
</Link>
</div>
@ -284,9 +324,9 @@ const Users = () => {
</div>
{/* /Filter */}
<div
className={`card${isFilterVisible ? ' visible' : ''}`}
className={`card${isFilterVisible ? " visible" : ""}`}
id="filter_inputs"
style={{ display: isFilterVisible ? 'block' : 'none' }}
style={{ display: isFilterVisible ? "block" : "none" }}
>
<div className="card-body pb-0">
<div className="row">
@ -327,7 +367,10 @@ const Users = () => {
<div className="input-blocks">
<a className="btn btn-filters ms-auto">
{" "}
<i data-feather="search" className="feather-search" />{" "}
<i
data-feather="search"
className="feather-search"
/>{" "}
Search{" "}
</a>
</div>
@ -338,17 +381,16 @@ const Users = () => {
{/* /Filter */}
<div className="table-responsive">
<Table columns={columns} dataSource={dataSource} />
</div>
</div>
</div>
{/* /product list */}
</div>
</div>
<AddUsers/>
<EditUser/>
<AddUsers />
<EditUser />
</div>
)
}
);
};
export default Users
export default Users;

View File

@ -15,6 +15,7 @@ import '../src/style/icons/fontawesome/css/all.min.css'
import { Provider } from "react-redux";
import store from "./core/redux/store.jsx";
import AllRoutes from "./Router/router.jsx";
import authApi from "./services/authApi";
const rootElement = document.getElementById('root');
@ -49,6 +50,9 @@ const initializeTheme = () => {
// Initialize theme before rendering
initializeTheme();
// Check authentication status on app startup
authApi.checkAuthStatus();
if (rootElement) {

View File

@ -41,6 +41,7 @@ api.interceptors.response.use(
// Unauthorized - redirect to login or refresh token
localStorage.removeItem('authToken');
// You can add redirect logic here
window.location.href = '/signin';
} else if (error.response?.status === 500) {
// Server error
console.error('Server Error:', error.response.data);

64
src/services/authApi.js Normal file
View File

@ -0,0 +1,64 @@
import { store } from '../core/redux/store';
import { loginStart, loginSuccess, loginFailure, logout, checkAuth } from '../core/redux/reducers/authReducer';
import api from './api';
const ENDPOINTS = {
LOGIN: 'auth/login',
LOGOUT: 'auth/logout',
};
export const authApi = {
// Check if user is authenticated
isAuthenticated() {
const token = localStorage.getItem('authToken');
const user = localStorage.getItem('user');
return !!(token && user);
},
// Get current user
getCurrentUser() {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
},
// Get auth token
getToken() {
return localStorage.getItem('authToken');
},
// Login function
async login(credentials) {
try {
store.dispatch(loginStart());
const response = await api.post(ENDPOINTS.LOGIN, credentials);
store.dispatch(loginSuccess(response.data));
return response.data;
} catch (error) {
store.dispatch(loginFailure(error.message));
throw error;
}
},
// Logout function
logout() {
store.dispatch(logout());
},
// Check authentication status on app start
checkAuthStatus() {
store.dispatch(checkAuth());
},
// Set auth headers for API requests
getAuthHeaders() {
const token = this.getToken();
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
}
}
export default authApi

View File

@ -0,0 +1,116 @@
import api from './api';
// Categories API endpoints
const ENDPOINTS = {
CATEGORIES: 'categories',
CATEGORY_BY_ID: (id) => `categories/${id}`,
CATEGORY_PRODUCTS: (id) => `categories/${id}/products`,
SEARCH: 'categories/search',
};
// Categories API service
export const categoriesApi = {
// Get all categories
getAllCategories: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.CATEGORIES, { params });
return response.data;
} catch (error) {
console.error('Error fetching categories:', error);
throw error;
}
},
// Get category by ID
getCategoryById: async (id) => {
try {
const response = await api.get(ENDPOINTS.CATEGORY_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching category ${id}:`, error);
throw error;
}
},
// Create new category
createCategory: async (categoryData) => {
try {
const response = await api.post(ENDPOINTS.CATEGORIES, categoryData);
return response.data;
} catch (error) {
console.error('Error creating category:', error);
throw error;
}
},
// Update category
updateCategory: async (id, categoryData) => {
try {
const response = await api.put(ENDPOINTS.CATEGORY_BY_ID(id), categoryData);
return response.data;
} catch (error) {
console.error(`Error updating category ${id}:`, error);
throw error;
}
},
// Delete category
deleteCategory: async (id) => {
try {
const response = await api.delete(ENDPOINTS.CATEGORY_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting category ${id}:`, error);
throw error;
}
},
// Search categories
searchCategories: async (query, params = {}) => {
try {
const response = await api.get(ENDPOINTS.SEARCH, {
params: { q: query, ...params }
});
return response.data;
} catch (error) {
console.error('Error searching categories:', error);
throw error;
}
},
// Get products by category
getCategoryProducts: async (id, params = {}) => {
try {
const response = await api.get(ENDPOINTS.CATEGORY_PRODUCTS(id), { params });
return response.data;
} catch (error) {
console.error(`Error fetching products for category ${id}:`, error);
throw error;
}
},
// Bulk operations
bulkUpdateCategories: async (categories) => {
try {
const response = await api.put(`${ENDPOINTS.CATEGORIES}/bulk`, { categories });
return response.data;
} catch (error) {
console.error('Error bulk updating categories:', error);
throw error;
}
},
bulkDeleteCategories: async (categoryIds) => {
try {
const response = await api.delete(`${ENDPOINTS.CATEGORIES}/bulk`, {
data: { ids: categoryIds }
});
return response.data;
} catch (error) {
console.error('Error bulk deleting categories:', error);
throw error;
}
},
};
export default categoriesApi;

104
src/services/ordersApi.js Normal file
View File

@ -0,0 +1,104 @@
import api from './api';
// Orders API endpoints
const ENDPOINTS = {
ORDERS: 'orders',
ORDER_BY_ID: (id) => `orders/${id}`,
SEARCH: 'orders/search',
};
// Orders API service
export const ordersApi = {
// Get all orders
getAllOrders: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.ORDERS, { params });
return response.data;
} catch (error) {
console.error('Error fetching orders:', error);
throw error;
}
},
// Get order by ID
getOrderById: async (id) => {
try {
const response = await api.get(ENDPOINTS.ORDER_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching order ${id}:`, error);
throw error;
}
},
// Create new order
createOrder: async (orderData) => {
try {
const response = await api.post(ENDPOINTS.ORDERS, orderData);
return response.data;
} catch (error) {
console.error('Error creating order:', error);
throw error;
}
},
// Update order
updateOrder: async (id, orderData) => {
try {
const response = await api.put(ENDPOINTS.ORDER_BY_ID(id), orderData);
return response.data;
} catch (error) {
console.error(`Error updating order ${id}:`, error);
throw error;
}
},
// Delete order
deleteOrder: async (id) => {
try {
const response = await api.delete(ENDPOINTS.ORDER_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting order ${id}:`, error);
throw error;
}
},
// Search orders
searchOrders: async (query, params = {}) => {
try {
const response = await api.get(ENDPOINTS.SEARCH, {
params: { q: query, ...params }
});
return response.data;
} catch (error) {
console.error('Error searching orders:', error);
throw error;
}
},
// Bulk operations
bulkUpdateOrders: async (orders) => {
try {
const response = await api.put(`${ENDPOINTS.ORDERS}/bulk`, { orders });
return response.data;
} catch (error) {
console.error('Error bulk updating orders:', error);
throw error;
}
},
bulkDeleteOrders: async (orderIds) => {
try {
const response = await api.delete(`${ENDPOINTS.ORDERS}/bulk`, {
data: { ids: orderIds }
});
return response.data;
} catch (error) {
console.error('Error bulk deleting orders:', error);
throw error;
}
},
};
export default ordersApi;

View File

@ -2,11 +2,11 @@ import api from './api';
// Products API endpoints
const ENDPOINTS = {
PRODUCTS: 'Products',
PRODUCT_BY_ID: (id) => `Products/${id}`,
CATEGORIES: 'Products/categories',
BRANDS: 'Products/brands',
SEARCH: 'Products/search',
PRODUCTS: 'products',
PRODUCT_BY_ID: (id) => `products/${id}`,
CATEGORIES: 'products/categories',
BRANDS: 'products/brands',
SEARCH: 'products/search',
};
// Products API service

228
src/services/usersApi.js Normal file
View File

@ -0,0 +1,228 @@
import api from './api';
// Users API endpoints
const ENDPOINTS = {
USERS: 'users',
USER_BY_ID: (id) => `users/${id}`,
USER_PROFILE: 'users/profile',
USER_PERMISSIONS: (id) => `users/${id}/permissions`,
USER_ROLES: (id) => `users/${id}/roles`,
SEARCH: 'users/search',
BULK_OPERATIONS: 'users/bulk',
};
// Users API service
export const usersApi = {
// Get all users
getAllUsers: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.USERS, { params });
return response.data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
},
// Get user by ID
getUserById: async (id) => {
try {
const response = await api.get(ENDPOINTS.USER_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching user ${id}:`, error);
throw error;
}
},
// Create new user
createUser: async (userData) => {
try {
const response = await api.post(ENDPOINTS.USERS, userData);
return response.data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
},
// Update user
updateUser: async (id, userData) => {
try {
const response = await api.put(ENDPOINTS.USER_BY_ID(id), userData);
return response.data;
} catch (error) {
console.error(`Error updating user ${id}:`, error);
throw error;
}
},
// Delete user
deleteUser: async (id) => {
try {
const response = await api.delete(ENDPOINTS.USER_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting user ${id}:`, error);
throw error;
}
},
// Search users
searchUsers: async (query, params = {}) => {
try {
const response = await api.get(ENDPOINTS.SEARCH, {
params: { q: query, ...params }
});
return response.data;
} catch (error) {
console.error('Error searching users:', error);
throw error;
}
},
// Get user profile
getUserProfile: async () => {
try {
const response = await api.get(ENDPOINTS.USER_PROFILE);
return response.data;
} catch (error) {
console.error('Error fetching user profile:', error);
throw error;
}
},
// Update user profile
updateUserProfile: async (profileData) => {
try {
const response = await api.put(ENDPOINTS.USER_PROFILE, profileData);
return response.data;
} catch (error) {
console.error('Error updating user profile:', error);
throw error;
}
},
// Get user permissions
getUserPermissions: async (id) => {
try {
const response = await api.get(ENDPOINTS.USER_PERMISSIONS(id));
return response.data;
} catch (error) {
console.error(`Error fetching user permissions ${id}:`, error);
throw error;
}
},
// Update user permissions
updateUserPermissions: async (id, permissions) => {
try {
const response = await api.put(ENDPOINTS.USER_PERMISSIONS(id), { permissions });
return response.data;
} catch (error) {
console.error(`Error updating user permissions ${id}:`, error);
throw error;
}
},
// Get user roles
getUserRoles: async (id) => {
try {
const response = await api.get(ENDPOINTS.USER_ROLES(id));
return response.data;
} catch (error) {
console.error(`Error fetching user roles ${id}:`, error);
throw error;
}
},
// Update user roles
updateUserRoles: async (id, roles) => {
try {
const response = await api.put(ENDPOINTS.USER_ROLES(id), { roles });
return response.data;
} catch (error) {
console.error(`Error updating user roles ${id}:`, error);
throw error;
}
},
// Change user password
changeUserPassword: async (id, passwordData) => {
try {
const response = await api.put(`${ENDPOINTS.USER_BY_ID(id)}/password`, passwordData);
return response.data;
} catch (error) {
console.error(`Error changing user password ${id}:`, error);
throw error;
}
},
// Activate/Deactivate user
toggleUserStatus: async (id, status) => {
try {
const response = await api.put(`${ENDPOINTS.USER_BY_ID(id)}/status`, { status });
return response.data;
} catch (error) {
console.error(`Error toggling user status ${id}:`, error);
throw error;
}
},
// Bulk operations
bulkUpdateUsers: async (users) => {
try {
const response = await api.put(ENDPOINTS.BULK_OPERATIONS, { users });
return response.data;
} catch (error) {
console.error('Error bulk updating users:', error);
throw error;
}
},
bulkDeleteUsers: async (userIds) => {
try {
const response = await api.delete(ENDPOINTS.BULK_OPERATIONS, {
data: { ids: userIds }
});
return response.data;
} catch (error) {
console.error('Error bulk deleting users:', error);
throw error;
}
},
// Export users
exportUsers: async (params = {}) => {
try {
const response = await api.get(`${ENDPOINTS.USERS}/export`, {
params,
responseType: 'blob'
});
return response.data;
} catch (error) {
console.error('Error exporting users:', error);
throw error;
}
},
// Import users
importUsers: async (fileData) => {
try {
const formData = new FormData();
formData.append('file', fileData);
const response = await api.post(`${ENDPOINTS.USERS}/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
} catch (error) {
console.error('Error importing users:', error);
throw error;
}
},
};
export default usersApi;

View File

@ -709,3 +709,10 @@ button {
border-color: $dark;
}
}
.center-vertical {
position: absolute;
top: 50%;
right: 1%;
transform: translateY(-50%);
}

View File

@ -26,3 +26,8 @@
@include box-shadow(null, 0, 2px, 3px, null, rgb(215, 197, 255));
}
}
.icon-small {
width: 18px;
height: 18px;
}

View File

@ -17,7 +17,8 @@
background-color: $primary;
border-color: $primary;
}
.disabled>.page-link, .page-link.disabled {
.disabled > .page-link,
.page-link.disabled {
color: $text-color;
background-color: $white;
border-color: $border-color;
@ -25,18 +26,18 @@
}
[dir="rtl"] {
.pagination{
.page-link{
.bx-chevron-left::before{
.pagination {
.page-link {
.bx-chevron-left::before {
content: "\ea50";
}
.bx-chevron-right::before{
.bx-chevron-right::before {
content: "\ea4d";
}
.ri-arrow-right-s-line:before{
.ri-arrow-right-s-line:before {
content: "\ea64";
}
.ri-arrow-left-s-line:before{
.ri-arrow-left-s-line:before {
content: "\ea6e";
}
}
@ -169,3 +170,23 @@
}
}
}
.custom-pagination .page-item .page-link {
border: none;
background: transparent;
}
.custom-pagination .page-item.active .page-link {
width: 36px;
height: 36px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
}
.page-link.disabled {
pointer-events: none;
opacity: 0.5;
cursor: not-allowed;
}

View File

@ -317,7 +317,7 @@ caption {
}
}
a {
color: $secondary;
color: $primary;
font-size: $font-size-14;
font-weight: $font-weight-normal;
line-height: normal;
@ -533,10 +533,10 @@ caption {
padding: 0;
}
.table-top {
padding: 24px 24px 0;
padding: 18px 18px 0;
}
.table-responsive {
padding: 24px;
padding: 18px;
border-top: 1px solid $gray-400;
.dataTables_wrapper {
border: 0;
@ -545,7 +545,7 @@ caption {
}
.tabs-set {
.nav-tabs {
padding: 24px 24px 0;
padding: 18px 18px 0;
margin-bottom: 0;
}
}
@ -674,3 +674,17 @@ table {
}
}
}
.custom-table .ant-table-thead > tr > th {
padding-top: 12px;
padding-bottom: 12px;
}
.custom-table .ant-table-thead .ant-table-cell {
background-color: #fafbfe;
}
.custom-table .ant-table-tbody > tr > td {
padding-top: 1px;
padding-bottom: 1px;
}

View File

@ -112,7 +112,7 @@
}
h3{
font-weight: $font-weight-bold;
color: $secondary;
color: $primary;
font-size: $font-size-18;
@include respond-below(custom991) {
font-size: $font-size-base;
@ -120,7 +120,7 @@
}
h4 {
font-weight: $font-weight-bold;
color: $secondary;
color: $primary;
font-size: $font-size-18;
margin-bottom: 5px;
@include respond-below(custom991) {
@ -536,7 +536,7 @@ th,td {
h4 {
font-size: $font-size-base;
font-weight: $font-weight-medium;
color: $secondary
color: $primary
}
}
&.image-upload-new{
@ -652,7 +652,7 @@ th,td {
}
label{
margin-bottom: 8px;
color: $secondary;
color: $primary;
font-weight: $font-weight-medium;
font-size: $font-size-base;
display: block;

View File

@ -38,7 +38,7 @@
color: $white;
}
svg {
color: #FE9F43
color: $primary
}
}
svg {
@ -66,21 +66,21 @@
}
&:hover{
background: rgba(254, 159, 67, 0.08);
color: #FE9F43;
color: $primary;
border-radius: 5px;
img {
filter: invert(72%) sepia(76%) saturate(1430%) hue-rotate(327deg) brightness(103%) contrast(101%);
}
span{
color: #FE9F43;
color: $primary;
}
svg{
color: #FE9F43;
color: $primary;
}
}
&.active{
background: rgba(254, 159, 67, 0.08);
color: #FE9F43;
color: $primary;
border-radius: 5px;
svg{
color: $white;
@ -89,12 +89,12 @@
filter: invert(72%) sepia(76%) saturate(1430%) hue-rotate(327deg) brightness(103%) contrast(101%);
}
span{
color: #FE9F43;
color: $primary;
}
.menu-arrow{
background: #FFEDDC;
&::before{
border-color: #FE9F43;
border-color: $primary;
}
}
}
@ -112,7 +112,7 @@
filter: invert(72%) sepia(76%) saturate(1430%) hue-rotate(327deg) brightness(103%) contrast(101%);
}
span{
color: #FE9F43;
color: $primary;
}
}
}
@ -132,8 +132,8 @@
&.active{
color: $secondary;
&:after{
background: #FE9F43;
border: 2px solid #FDB;
background: $primary;
border: 2px solid $secondary;
}
}
&::after{
@ -148,8 +148,8 @@
&:hover{
color:$primary;
&:after{
background: #FE9F43;
border: 2px solid #FDB;
background: $primary;
border: 2px solid $secondary;
}
}
}
@ -188,15 +188,15 @@
}
}
&:after{
background: #FE9F43;
border: 2px solid #FDB;
background: $primary;
border: 2px solid $secondary;
}
}
&:hover{
color:$primary;
&:after{
background: #FE9F43;
border: 2px solid #FDB;
background: $primary;
border: 2px solid $secondary;
}
span {
color:$primary;
@ -209,7 +209,7 @@
&.active a{
background: rgba(254, 159, 67, 0.08);
border-radius: 5px;
color: #FE9F43;
color: $primary;
span {
color: $primary;
}
@ -240,25 +240,25 @@
li {
&.active{
a{
color: #FE9F43;
color: $primary;
}
svg {
color: #FE9F43
color: $primary
}
}
.submenu > {
a {
&.active{
background: rgba(254, 159, 67, 0.08);
color: #FE9F43;
background: $secondary;
color: $primary;
border-radius: 5px;
span{
color: #FE9F43;
color: $primary;
}
.menu-arrow{
background: #FFEDDC;
// background: #FFEDDC;
&::before{
border-color: #FE9F43;
border-color: $primary;
}
}
}
@ -321,8 +321,8 @@
&:hover{
color:$primary;
&:after{
background: #FE9F43;
border: 2px solid #FDB;
background: $primary;
border: 2px solid $secondary;
}
}
}

View File

@ -854,10 +854,10 @@ $__basecolor: #2c3038;
// Badge styling in options
.badge {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
// display: inline-block;
// width: 8px;
// height: 8px;
// border-radius: 50%;
&.badge-blue { background: #007bff; }
&.badge-green { background: #28a745; }

View File

@ -63,7 +63,7 @@
margin-top: 50px !important;
p {
font-size: $font-size-14;
color: $secondary;
color: $primary;
margin-bottom: 0;
font-weight: $font-weight-normal;
}
@ -120,7 +120,7 @@
h4 {
font-size: $font-size-15;
font-weight: $font-weight-normal;
color: $secondary;
color: $primary;
line-height: 1.4;
}
.verfy-mail-content {
@ -132,7 +132,7 @@
margin-bottom: 15px;
label {
width: 100%;
color: $secondary;
color: $primary;
margin-bottom:10px;
font-size: $font-size-15;
font-weight: $font-weight-normal;
@ -209,12 +209,12 @@
h4{
font-size: $font-size-15;
font-weight: $font-weight-normal;
color: $secondary;
color: $primary;
@include respond-below(custom575) {
font-size: $font-size-base;
}
a{
color: $secondary;
color: $primary;
font-weight: $font-weight-bold;
font-size: $font-size-14;
}
@ -339,7 +339,7 @@
}
}
a {
color: $secondary;
color: $primary;
width: 100%;
border: 1px solid rgba(145, 158, 171, 0.23);
background: $white;

View File

@ -182,7 +182,7 @@
border: 0;
}
h4 {
color: $secondary;
color: $primary;
font-size: $font-size-base;
font-weight: $font-weight-medium;
width: 30%;
@ -246,14 +246,14 @@
h4 {
font-size: $font-size-base;
color: $secondary;
color: $primary;
font-weight: $font-weight-medium;
}
h6 {
font-size: $font-size-13;
font-weight: $font-weight-normal;
color: $secondary;
color: $primary;
}
}
@ -409,7 +409,7 @@ span {
width: 40px;
height: 40px;
border: 1px solid var(--Stroke, rgba(145, 158, 171, 0.30));
background: $secondary;
background: $primary;
@include rounded(8px);
@include respond-below(custom575) {
position: relative;

View File

@ -16,9 +16,9 @@ $font-family-secondary: "Poppins", sans-serif;
$font-awesome: "Fontawesome";
// Theme Colors Variables
$primary: #FF9F43;
$primary: #36175e;
$primary-hover: darken($primary, 10%);
$secondary: #092C4C;
$secondary: #f1eaf9;
$secondary-hover: darken($secondary, 10%);
$success: #28C76F;
$success-hover: darken($success, 10%);
@ -29,7 +29,7 @@ $warning-hover: darken($warning, 10%);
$danger: #FF0000;
$danger-hover: darken($danger, 10%);
$dark: #29344a;
$light: #f8f9fa;
$light: #FAFBFE;
$white: #ffffff;
$black: #000000;
$purple: #7367F0;
@ -44,26 +44,26 @@ $indigo: #4d5ddb;
$yellow: #ffff00;
// Primary
$primary-100: #FFF5EC;
$primary-200: #FFECD9;
$primary-300: #FFE2C7;
$primary-400: #FFD9B4;
$primary-500: #FFCFA1;
$primary-600: #FFC58E;
$primary-700: #FFBC7B;
$primary-800: #FFB269;
$primary-900: #FFA956;
$primary-100: #ede7f3;
$primary-200: #d3c2e3;
$primary-300: #b99cd3;
$primary-400: #9f76c3;
$primary-500: #8551b3;
$primary-600: #6e3f98;
$primary-700: #57317a;
$primary-800: #40225c;
$primary-900: #2a153d;
// Secondary
$secondary-100: #E6EAED;
$secondary-200: #CED5DB;
$secondary-300: #B5C0C9;
$secondary-400: #9DABB7;
$secondary-500: #8496A6;
$secondary-600: #6B8094;
$secondary-700: #536B82;
$secondary-800: #3A5670;
$secondary-900: #22415E;
$secondary-100: #fdfbff;
$secondary-200: #f8f4fc;
$secondary-300: #f1eaf9;
$secondary-400: #e0d3f1;
$secondary-500: #c8b0e6;
$secondary-600: #af8ddb;
$secondary-700: #9369cc;
$secondary-800: #7547aa;
$secondary-900: #563180;
// Success
$success-100: #EAF9F1;
@ -158,7 +158,7 @@ $theme-colors: (
"black": $black,
"purple": $purple,
"yellow": $yellow,
"teal": $teal
"teal": $teal,
);
$text-color: #5B6670;

9
src/utils/currency.js Normal file
View File

@ -0,0 +1,9 @@
const formatRupiah = (angka) => {
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
}).format(angka);
};
export { formatRupiah };

13
src/utils/date.js Normal file
View File

@ -0,0 +1,13 @@
const formatDate = (isoDate) => {
const date = new Date(isoDate);
const formatted = date.toLocaleString("en-GB", {
day: "2-digit",
month: "short", // example: July
year: "numeric",
});
return formatted
};
export { formatDate };