Compare commits

...

3 Commits

Author SHA1 Message Date
ferdiansyah783
86a4a19209 refactor and conneting api 2025-08-03 20:55:11 +07:00
ferdiansyah783
0656cda2ec add superadmin authentication 2025-08-03 11:14:55 +07:00
ferdiansyah783
a40a1994e5 update 3 modules 2025-08-02 02:33:10 +07:00
81 changed files with 10690 additions and 6631 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) # CORS Proxy for development (uncomment if needed)
# REACT_APP_API_BASE_URL=https://cors-anywhere.herokuapp.com/https://trantran.zenstores.com.vn/api/ # 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", "name": "my-app",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1",
"@ckeditor/ckeditor5-build-classic": "^41.2.0", "@ckeditor/ckeditor5-build-classic": "^41.2.0",
"@ckeditor/ckeditor5-react": "^6.2.0", "@ckeditor/ckeditor5-react": "^6.2.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
@ -142,13 +143,14 @@
} }
}, },
"node_modules/@ant-design/icons": { "node_modules/@ant-design/icons": {
"version": "5.3.3", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.3.tgz", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
"integrity": "sha512-Zfci1s4f4+vfpVD6ksmmPuBv00SB/slpUAQlsBlMeRJdSleVVkgTUdlBM4j/vGzqYfMh2hF8/Poa1VSh542w0Q==", "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/colors": "^7.0.0", "@ant-design/colors": "^7.0.0",
"@ant-design/icons-svg": "^4.4.0", "@ant-design/icons-svg": "^4.4.0",
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.24.8",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"rc-util": "^5.31.1" "rc-util": "^5.31.1"
}, },
@ -2042,12 +2044,10 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.24.0", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"dependencies": { "license": "MIT",
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -21112,11 +21112,6 @@
"node": ">=4" "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": { "node_modules/regenerator-transform": {
"version": "0.15.2", "version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",

View File

@ -4,6 +4,7 @@
"homepage": "/", "homepage": "/",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.6.1",
"@ckeditor/ckeditor5-build-classic": "^41.2.0", "@ckeditor/ckeditor5-build-classic": "^41.2.0",
"@ckeditor/ckeditor5-react": "^6.2.0", "@ckeditor/ckeditor5-react": "^6.2.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="icon" href="./favicon.png" /> <link rel="icon" href="./favicon.png" />
<title>Dreams Pos admin template</title> <title>APSKEL</title>
</head> </head>
<body> <body>

View File

@ -1,16 +1,25 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
import React, { useEffect, useState } from "react"; 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 FeatherIcon from "feather-icons-react";
import ImageWithBasePath from "../../core/img/imagewithbasebath"; import ImageWithBasePath from "../../core/img/imagewithbasebath";
import { Search, XCircle } from "react-feather"; import { Search, XCircle } from "react-feather";
import { all_routes } from "../../Router/all_routes"; import { all_routes } from "../../Router/all_routes";
import { useSelector } from "react-redux";
import authApi from "../../services/authApi";
const Header = () => { const Header = () => {
const route = all_routes; const route = all_routes;
const navigate = useNavigate();
const authState = useSelector((state) => state.auth);
const [toggle, SetToggle] = useState(false); const [toggle, SetToggle] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const handleLogout = () => {
authApi.logout()
navigate(route.signin);
};
const isElementVisible = (element) => { const isElementVisible = (element) => {
return element.offsetWidth > 0 || element.offsetHeight > 0; return element.offsetWidth > 0 || element.offsetHeight > 0;
}; };
@ -616,8 +625,8 @@ const Header = () => {
/> />
</span> </span>
<span className="user-detail"> <span className="user-detail">
<span className="user-name">John Smilga</span> <span className="user-name">{authState.user?.name || 'User'}</span>
<span className="user-role">Super Admin</span> <span className="user-role">{authState.user?.role || 'Admin'}</span>
</span> </span>
</span> </span>
</Link> </Link>
@ -632,12 +641,12 @@ const Header = () => {
<span className="status online" /> <span className="status online" />
</span> </span>
<div className="profilesets"> <div className="profilesets">
<h6>John Smilga</h6> <h6>{authState.user?.name || 'User'}</h6>
<h5>Super Admin</h5> <h5>{authState.user?.role || 'Admin'}</h5>
</div> </div>
</div> </div>
<hr className="m-0" /> <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 <i className="me-2" data-feather="user" /> My Profile
</Link> </Link>
<Link className="dropdown-item" to={route.generalsettings}> <Link className="dropdown-item" to={route.generalsettings}>
@ -645,14 +654,18 @@ const Header = () => {
Settings Settings
</Link> </Link>
<hr className="m-0" /> <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 <ImageWithBasePath
src="assets/img/icons/log-out.svg" src="assets/img/icons/log-out.svg"
alt="img" alt="img"
className="me-2" className="me-2"
/> />
Logout Logout
</Link> </button>
</div> </div>
</div> </div>
</li> </li>
@ -675,9 +688,13 @@ const Header = () => {
<Link className="dropdown-item" to="generalsettings"> <Link className="dropdown-item" to="generalsettings">
Settings Settings
</Link> </Link>
<Link className="dropdown-item" to="signin"> <button
className="dropdown-item"
onClick={handleLogout}
style={{ background: 'none', border: 'none', width: '100%', textAlign: 'left' }}
>
Logout Logout
</Link> </button>
</div> </div>
</div> </div>
{/* /Mobile Menu */} {/* /Mobile Menu */}

View File

@ -1,19 +1,16 @@
import React, { useState } from "react"; import { useState } from "react";
import Scrollbars from "react-custom-scrollbars-2"; import Scrollbars from "react-custom-scrollbars-2";
// import { useSelector } from "react-redux";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { SidebarData } from "../../core/json/siderbar_data"; import { SidebarData } from "../../core/json/siderbar_data";
import HorizontalSidebar from "./horizontalSidebar";
import CollapsedSidebar from "./collapsedSidebar"; import CollapsedSidebar from "./collapsedSidebar";
import HorizontalSidebar from "./horizontalSidebar";
import { useSelector } from "react-redux";
const Sidebar = () => { const Sidebar = () => {
// const SidebarData = useSelector((state) => state.sidebar_data); const { user } = useSelector((state) => state.auth);
// console.log(sidebarData, "sidebar");
const Location = useLocation(); const Location = useLocation();
console.log("Location.pathname", Location.pathname);
const [subOpen, setSubopen] = useState(""); const [subOpen, setSubopen] = useState("");
const [subsidebar, setSubsidebar] = useState(""); const [subsidebar, setSubsidebar] = useState("");
@ -46,6 +43,12 @@ const Sidebar = () => {
<ul> <ul>
{mainLabel?.submenuItems?.map((title, i) => { {mainLabel?.submenuItems?.map((title, i) => {
if (title?.role) {
if (user?.role !== title?.role) {
return null;
}
}
let link_array = []; let link_array = [];
title?.submenuItems?.map((link) => { title?.submenuItems?.map((link) => {
link_array?.push(link?.link); link_array?.push(link?.link);

View File

@ -54,7 +54,7 @@ export const all_routes = {
floatinglabel: "/form-floating-labels", floatinglabel: "/form-floating-labels",
formvalidation: "/form-validation", formvalidation: "/form-validation",
select2: "/form-select2", select2: "/form-select2",
companylist: "/company-list",
toasts: "/ui-toasts", toasts: "/ui-toasts",
video: "/ui-video", video: "/ui-video",
sweetalerts: "/ui-sweetalerts", sweetalerts: "/ui-sweetalerts",
@ -81,7 +81,7 @@ export const all_routes = {
typicons: "/icon-typicon", typicons: "/icon-typicon",
flagicons: "/icon-flag", flagicons: "/icon-flag",
ribbon: "/ui-ribbon", ribbon: "/ui-ribbon",
paymentmethodlist: "/payment-method-list",
chat: "/chat", chat: "/chat",
videocall: "/video-call", videocall: "/video-call",
audiocall: "/audio-call", audiocall: "/audio-call",

View File

@ -1,17 +1,18 @@
import React from "react"; import { useSelector } from "react-redux";
import { Route, Routes } from "react-router-dom"; import { Outlet, Route, Routes } from "react-router-dom";
import Header from "../InitialPage/Sidebar/Header"; import Header from "../InitialPage/Sidebar/Header";
import Sidebar from "../InitialPage/Sidebar/Sidebar"; import Sidebar from "../InitialPage/Sidebar/Sidebar";
import { pagesRoute, posRoutes, publicRoutes } from "./router.link";
import { Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import ThemeSettings from "../InitialPage/themeSettings"; import ThemeSettings from "../InitialPage/themeSettings";
import ProtectedRoute from "../components/ProtectedRoute";
import { pagesRoute, posRoutes, publicRoutes } from "./router.link";
// import CollapsedSidebar from "../InitialPage/Sidebar/collapsedSidebar"; // import CollapsedSidebar from "../InitialPage/Sidebar/collapsedSidebar";
import GuestRoute from "../components/GuestRoute";
import Loader from "../feature-module/loader/loader"; import Loader from "../feature-module/loader/loader";
// import HorizontalSidebar from "../InitialPage/Sidebar/horizontalSidebar"; // import HorizontalSidebar from "../InitialPage/Sidebar/horizontalSidebar";
//import LoadingSpinner from "../InitialPage/Sidebar/LoadingSpinner"; //import LoadingSpinner from "../InitialPage/Sidebar/LoadingSpinner";
const AllRoutes = () => { const AllRoutes = () => {
const { user } = useSelector((state) => state.auth);
const data = useSelector((state) => state.toggle_header); const data = useSelector((state) => state.toggle_header);
// const layoutStyles = useSelector((state) => state.layoutstyledata); // const layoutStyles = useSelector((state) => state.layoutstyledata);
const HeaderLayout = () => ( const HeaderLayout = () => (
@ -48,8 +49,6 @@ const AllRoutes = () => {
</div> </div>
); );
console.log(publicRoutes, "dashboard");
return ( return (
<div> <div>
<Routes> <Routes>
@ -58,13 +57,29 @@ const AllRoutes = () => {
<Route path={route.path} element={route.element} key={id} /> <Route path={route.path} element={route.element} key={id} />
))} ))}
</Route> </Route>
<Route path={"/"} element={<HeaderLayout />}>
{publicRoutes.map((route, id) => ( <Route
<Route path={route.path} element={route.element} key={id} /> path={"/"}
))} element={
<ProtectedRoute>
<HeaderLayout />
</ProtectedRoute>
}
>
{publicRoutes.map((route, id) => {
if (route?.role && route?.role !== user?.role) return null;
return <Route path={route.path} element={route.element} key={id} />;
})}
</Route> </Route>
<Route path={"/"} element={<Authpages />}> <Route
path={"/"}
element={
<GuestRoute>
<Authpages />
</GuestRoute>
}
>
{pagesRoute.map((route, id) => ( {pagesRoute.map((route, id) => (
<Route path={route.path} element={route.element} key={id} /> <Route path={route.path} element={route.element} key={id} />
))} ))}

View File

@ -204,6 +204,9 @@ import AddWeddingGuest from "../feature-module/inventory/addWeddingGuest";
import ProductList2 from "../feature-module/inventory/productlist2"; import ProductList2 from "../feature-module/inventory/productlist2";
import ProductList3 from "../feature-module/inventory/productlist3"; import ProductList3 from "../feature-module/inventory/productlist3";
import { all_routes } from "./all_routes"; import { all_routes } from "./all_routes";
import PaymentMethodList from "../feature-module/FinanceAccounts/paymentmethodlist";
import CompanyList from "../feature-module/superadmin/companylist";
export const publicRoutes = [ export const publicRoutes = [
{ {
id: 1, id: 1,
@ -1404,6 +1407,13 @@ export const publicRoutes = [
element: <ProductDetail />, element: <ProductDetail />,
route: Route, route: Route,
}, },
{
id: 113.1,
path: `${routes.productdetails}/:id`,
name: "productdetails",
element: <ProductDetail />,
route: Route,
},
{ {
id: 114, id: 114,
path: routes.warehouses, path: routes.warehouses,
@ -1481,7 +1491,23 @@ export const publicRoutes = [
element: <Navigate to="/" />, element: <Navigate to="/" />,
route: Route, route: Route,
}, },
{
id: 120,
path: routes.paymentmethodlist,
name: "paymendMethodList",
element: <PaymentMethodList /> ,
route: Route,
},
{
id: 1,
path: routes.companylist,
name: "companies",
element: <CompanyList />,
route: Route,
role: 'superadmin'
}
]; ];
export const posRoutes = [ export const posRoutes = [
{ {
id: 1, id: 1,

View File

@ -1,5 +1,11 @@
import React, { useState, useEffect } from 'react'; import {
import { useSelector } from 'react-redux'; DoubleLeftOutlined,
DoubleRightOutlined,
LeftOutlined,
RightOutlined,
} from "@ant-design/icons";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
const CustomPagination = ({ const CustomPagination = ({
currentPage = 1, currentPage = 1,
@ -13,28 +19,32 @@ const CustomPagination = ({
showInfo = true, showInfo = true,
showPageSizeSelector = true, showPageSizeSelector = true,
compact = false, compact = false,
className = '' className = "",
}) => { }) => {
// Theme state for force re-render // Theme state for force re-render
const [themeKey, setThemeKey] = useState(0); const [themeKey, setThemeKey] = useState(0);
// Get theme from Redux and localStorage fallback // Get theme from Redux and localStorage fallback
const reduxTheme = useSelector((state) => state.theme?.isDarkMode); const reduxTheme = useSelector((state) => state.theme?.isDarkMode);
const localStorageTheme = localStorage.getItem('colorschema') === 'dark_mode'; const localStorageTheme = localStorage.getItem("colorschema") === "dark_mode";
const documentTheme = document.documentElement.getAttribute('data-layout-mode') === 'dark_mode'; const documentTheme =
document.documentElement.getAttribute("data-layout-mode") === "dark_mode";
const isDarkMode = reduxTheme || localStorageTheme || documentTheme; const isDarkMode = reduxTheme || localStorageTheme || documentTheme;
// Listen for theme changes // Listen for theme changes
useEffect(() => { useEffect(() => {
const handleThemeChange = () => { const handleThemeChange = () => {
setThemeKey(prev => prev + 1); setThemeKey((prev) => prev + 1);
}; };
// Listen for data-layout-mode attribute changes // Listen for data-layout-mode attribute changes
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-layout-mode') { if (
mutation.type === "attributes" &&
mutation.attributeName === "data-layout-mode"
) {
handleThemeChange(); handleThemeChange();
} }
}); });
@ -42,27 +52,25 @@ const CustomPagination = ({
observer.observe(document.documentElement, { observer.observe(document.documentElement, {
attributes: true, attributes: true,
attributeFilter: ['data-layout-mode'] attributeFilter: ["data-layout-mode"],
}); });
// Also listen for localStorage changes // Also listen for localStorage changes
const handleStorageChange = (e) => { const handleStorageChange = (e) => {
if (e.key === 'colorschema') { if (e.key === "colorschema") {
handleThemeChange(); handleThemeChange();
} }
}; };
window.addEventListener('storage', handleStorageChange); window.addEventListener("storage", handleStorageChange);
return () => { return () => {
observer.disconnect(); observer.disconnect();
window.removeEventListener('storage', handleStorageChange); window.removeEventListener("storage", handleStorageChange);
}; };
}, []); }, []);
// Calculate pagination info console.log(totalCount);
const startRecord = totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const endRecord = Math.min(currentPage * pageSize, totalCount);
// Handle page change // Handle page change
const handlePageClick = (page) => { const handlePageClick = (page) => {
@ -80,190 +88,191 @@ const CustomPagination = ({
// Container styles based on compact mode // Container styles based on compact mode
const containerStyles = { const containerStyles = {
background: isDarkMode background: isDarkMode
? 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)' ? "linear-gradient(135deg, #2c3e50 0%, #34495e 100%)"
: 'linear-gradient(135deg, #ffffff, #f8f9fa)', : "linear-gradient(135deg, #ffffff, #f8f9fa)",
border: isDarkMode border: isDarkMode
? '1px solid rgba(52, 152, 219, 0.3)' ? "1px solid rgba(52, 152, 219, 0.3)"
: '1px solid rgba(0, 0, 0, 0.1)', : "1px solid rgba(0, 0, 0, 0.1)",
borderRadius: compact ? '8px' : '12px', borderRadius: compact ? "8px" : "12px",
boxShadow: isDarkMode 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
: (compact ? '0 1px 6px rgba(0, 0, 0, 0.06)' : '0 2px 12px rgba(0, 0, 0, 0.08)'), ? "0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(52, 152, 219, 0.1)"
backdropFilter: isDarkMode ? (compact ? 'blur(8px)' : 'blur(10px)') : 'none', : "0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)"
transition: compact ? 'all 0.2s ease' : 'all 0.3s ease', : compact
position: 'relative', ? "0 1px 6px rgba(0, 0, 0, 0.06)"
overflow: 'hidden', : "0 2px 12px rgba(0, 0, 0, 0.08)",
padding: compact ? '8px 16px' : '16px 24px', backdropFilter: isDarkMode
margin: compact ? '8px 0' : '16px 0', ? compact
fontSize: compact ? '13px' : '14px' ? "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 ( return (
<div <div
key={`pagination-${themeKey}`} key={`pagination-${themeKey}`}
className={`custom-pagination-container ${isDarkMode ? '' : 'light-mode'} ${className}`} className={`custom-pagination-container ${
isDarkMode ? "" : "light-mode"
} ${className}`}
style={containerStyles} style={containerStyles}
> >
{/* Pagination Info */} {/* Pagination Info */}
{showInfo && ( {showInfo && (
<div <div
style={{ style={{
display: 'flex', display: "flex",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
marginBottom: compact ? '8px' : '16px', marginBottom: compact ? "8px" : "0px",
flexWrap: 'wrap', flexWrap: "wrap",
gap: compact ? '8px' : '12px' gap: compact ? "8px" : "12px",
}} }}
> >
{showPageSizeSelector && ( {showPageSizeSelector && (
<div style={{display: 'flex', alignItems: 'center', gap: compact ? '6px' : '12px'}}> <div
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>Số hàng mỗi trang</span> 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 <select
value={pageSize} value={pageSize}
onChange={(e) => handlePageSizeClick(parseInt(e.target.value))} onChange={(e) => handlePageSizeClick(parseInt(e.target.value))}
disabled={loading} disabled={loading}
style={{ style={{
background: loading background: loading
? (isDarkMode ? 'linear-gradient(45deg, #7f8c8d, #95a5a6)' : 'linear-gradient(135deg, #f8f9fa, #e9ecef)') ? isDarkMode
: (isDarkMode ? 'linear-gradient(45deg, #34495e, #2c3e50)' : 'linear-gradient(135deg, #ffffff, #f8f9fa)'), ? "linear-gradient(45deg, #7f8c8d, #95a5a6)"
border: isDarkMode : "linear-gradient(135deg, #f8f9fa, #e9ecef)"
? '1px solid rgba(52, 152, 219, 0.3)' : isDarkMode
: '1px solid #dee2e6', ? "linear-gradient(45deg, #34495e, #2c3e50)"
borderRadius: compact ? '4px' : '6px', : "linear-gradient(135deg, #ffffff, #f8f9fa)",
color: isDarkMode ? '#ffffff' : '#495057', border: isDarkMode
padding: compact ? '2px 6px' : '4px 8px', ? "1px solid rgba(52, 152, 219, 0.3)"
fontSize: compact ? '12px' : '14px', : "1px solid #dee2e6",
cursor: loading ? 'not-allowed' : 'pointer', 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, 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 <option
key={option} key={option}
value={option} value={option}
style={{background: isDarkMode ? '#2c3e50' : '#ffffff', color: isDarkMode ? '#ffffff' : '#495057'}} style={{
background: isDarkMode ? "#2c3e50" : "#ffffff",
color: isDarkMode ? "#ffffff" : "#495057",
}}
> >
{option} {option}
</option> </option>
))} ))}
</select> </select>
<span style={{color: isDarkMode ? '#bdc3c7' : '#2c3e50', fontSize: compact ? '12px' : '14px', fontWeight: '500'}}>bản ghi</span> <span
style={{
color: isDarkMode ? "#bdc3c7" : "#2c3e50",
fontSize: compact ? "12px" : "14px",
fontWeight: "500",
}}
>
records
</span>
</div> </div>
)} )}
<div style={{display: 'flex', alignItems: 'center', gap: compact ? '6px' : '12px'}}> {/* Pagination Buttons */}
<div <div
style={{ style={{
background: isDarkMode position: "relative",
? 'linear-gradient(45deg, #3498db, #2ecc71)' zIndex: 1,
: 'linear-gradient(45deg, #007bff, #28a745)', display: "flex",
borderRadius: '50%', justifyContent: "center",
width: compact ? '16px' : '24px', alignItems: "center",
height: compact ? '16px' : '24px', gap: compact ? "4px" : "8px",
display: 'flex', }}
alignItems: 'center', >
justifyContent: 'center', <nav aria-label="Custom pagination">
fontSize: compact ? '8px' : '12px', <ul className="pagination justify-content-center custom-pagination">
boxShadow: isDarkMode <li className="page-item">
? (compact ? '0 1px 4px rgba(52, 152, 219, 0.3)' : '0 2px 8px rgba(52, 152, 219, 0.3)') <a
: (compact ? '0 1px 4px rgba(0, 123, 255, 0.2)' : '0 2px 8px rgba(0, 123, 255, 0.2)'), onClick={() => handlePageClick(1)}
transition: compact ? 'all 0.2s ease' : 'all 0.3s ease' className="page-link"
}} aria-label="First"
> >
📊 <DoubleLeftOutlined
</div> style={{ fontSize: "12px", marginRight: "-10px" }}
<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 </a>
</span> </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> </div>
)} )}
{/* Pagination Buttons */}
<div
style={{
position: 'relative',
zIndex: 1,
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';
}
}}
>
{pageNum}
</button>
);
})}
</div>
</div> </div>
); );
}; };

View File

@ -0,0 +1,15 @@
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
const GuestRoute = ({ children }) => {
const authState = useSelector((state) => state.auth);
const isAuthenticated = authState?.isAuthenticated || authState?.token;
if (isAuthenticated) {
return <Navigate to="/" replace />;
}
return children;
};
export default GuestRoute;

View File

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

View File

@ -38,7 +38,7 @@
// Color variants // Color variants
&.primary { &.primary {
background: #ff9f43; background: #36175e;
color: white; color: white;
&:hover:not(.loading):not(.disabled) { &:hover:not(.loading):not(.disabled) {
@ -83,11 +83,11 @@
&.outline-primary { &.outline-primary {
background: transparent; background: transparent;
color: #ff9f43; color: #36175e;
border: 2px solid #ff9f43; border: 2px solid #36175e;
&:hover:not(.loading):not(.disabled) { &:hover:not(.loading):not(.disabled) {
background: #ff9f43; background: #36175e;
color: white; color: white;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 159, 67, 0.4); box-shadow: 0 4px 12px rgba(255, 159, 67, 0.4);

View File

@ -0,0 +1,18 @@
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children }) => {
// Check if user is authenticated using Redux state
const authState = useSelector((state) => state.auth);
const isAuthenticated = authState?.isAuthenticated || authState?.token;
if (!isAuthenticated) {
// Redirect to login page if not authenticated
return <Navigate to="/signin" replace />;
}
// If authenticated, render the protected component
return children;
};
export default ProtectedRoute;

File diff suppressed because it is too large Load Diff

View File

@ -1,72 +1,148 @@
import React from 'react' import { useState } from "react";
import { Link } from 'react-router-dom' import { useDispatch, useSelector } from "react-redux";
import Swal from "sweetalert2";
import {
createCategory,
fetchCategories,
} from "../../redux/actions/categoryActions";
const AddCategoryList = () => { const AddCategoryList = () => {
return ( const dispatch = useDispatch();
<div> const { creating } = useSelector((state) => state.categories);
{/* Add Category */}
<div className="modal fade" id="add-category">
<div className="modal-dialog modal-dialog-centered custom-modal-two">
<div className="modal-content">
<div className="page-wrapper-new p-0">
<div className="content">
<div className="modal-header border-0 custom-modal-header">
<div className="page-title">
<h4>Create Category</h4>
</div>
<button
type="button"
className="close"
data-bs-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body custom-modal-body">
<form>
<div className="mb-3">
<label className="form-label">Category</label>
<input type="text" className="form-control" />
</div>
<div className="mb-3">
<label className="form-label">Category Slug</label>
<input type="text" className="form-control" />
</div>
<div className="mb-0">
<div className="status-toggle modal-status d-flex justify-content-between align-items-center">
<span className="status-label">Status</span>
<input
type="checkbox"
id="user2"
className="check"
defaultChecked="true"
/>
<label htmlFor="user2" className="checktoggle" />
</div>
</div>
<div className="modal-footer-btn">
<button
type="button"
className="btn btn-cancel me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<Link to="#" className="btn btn-submit">
Create Category
</Link>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{/* /Add Category */}
</div>
)
}
export default AddCategoryList const [formData, setFormData] = useState({
name: "",
description: "",
});
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await dispatch(createCategory(formData));
await dispatch(fetchCategories());
Swal.fire({
title: "Success!",
text: "Category created successfully!",
icon: "success",
showConfirmButton: false,
timer: 1500,
}).then(() => {
const closeButton = document.querySelector("#add-category .btn-close");
closeButton.click();
});
} catch (error) {
console.error("Error creating category:", error);
// Show error message
Swal.fire({
icon: "error",
title: "Error!",
text: error.message || "Failed to create category. Please try again.",
});
}
};
return (
<div>
{/* Add Category */}
<div className="modal fade" id="add-category">
<div className="modal-dialog modal-dialog-centered custom-modal-two">
<div className="modal-content">
<div className="page-wrapper-new p-0">
<div className="content">
<div className="modal-header">
<div className="page-title">
<h4>Create Category</h4>
</div>
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div className="modal-body custom-modal-body">
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Category</label>
<input
type="text"
className="form-control border"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label">Description</label>
<input
type="text"
className="form-control border"
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">
<span className="status-label">Status</span>
<input
type="checkbox"
id="user2"
className="check"
defaultChecked="true"
/>
<label htmlFor="user2" className="checktoggle" />
</div>
</div>
<div className="modal-footer-btn">
<button
type="button"
className="btn btn-outline-dark me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<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>
</div>
</div>
</div>
</div>
</div>
{/* /Add Category */}
</div>
);
};
export default AddCategoryList;

View File

@ -1,80 +1,160 @@
import React from 'react' import { useEffect, useState } from "react";
import { Link } from 'react-router-dom' import { useDispatch, useSelector } from "react-redux";
import Swal from "sweetalert2";
import {
fetchCategories,
updateCategory,
} from "../../redux/actions/categoryActions";
const EditCategoryList = () => { const EditCategoryList = () => {
return ( const dispatch = useDispatch();
<div>
{/* Edit Category */}
<div className="modal fade" id="edit-category">
<div className="modal-dialog modal-dialog-centered custom-modal-two">
<div className="modal-content">
<div className="page-wrapper-new p-0">
<div className="content">
<div className="modal-header border-0 custom-modal-header">
<div className="page-title">
<h4>Edit Category</h4>
</div>
<button
type="button"
className="close"
data-bs-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
</div>
<div className="modal-body custom-modal-body">
<form>
<div className="mb-3">
<label className="form-label">Category</label>
<input
type="text"
className="form-control"
defaultValue="Laptop"
/>
</div>
<div className="mb-3">
<label className="form-label">Category Slug</label>
<input
type="text"
className="form-control"
defaultValue="laptop"
/>
</div>
<div className="mb-0">
<div className="status-toggle modal-status d-flex justify-content-between align-items-center">
<span className="status-label">Status</span>
<input
type="checkbox"
id="user3"
className="check"
defaultChecked="true"
/>
<label htmlFor="user3" className="checktoggle" />
</div>
</div>
<div className="modal-footer-btn">
<button
type="button"
className="btn btn-cancel me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<Link to="#" className="btn btn-submit">
Save Changes
</Link>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{/* /Edit Category */}
</div>
)
}
export default EditCategoryList const { currentCategory, updating, currentPage, pageSize } = useSelector(
(state) => state.categories
);
const [formData, setFormData] = useState({
name: "",
description: "",
});
useEffect(() => {
if (currentCategory) {
setFormData({
name: currentCategory.name,
description: currentCategory.description,
});
}
}, [currentCategory]);
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await dispatch(updateCategory(currentCategory?.id, formData));
await dispatch(fetchCategories({ page: currentPage, limit: pageSize }));
Swal.fire({
title: "Success!",
text: "Category updated successfully!",
icon: "success",
showConfirmButton: false,
timer: 1500,
}).then(() => {
const closeButton = document.querySelector("#edit-category .btn-close");
closeButton.click();
});
} catch (error) {
console.error("Error updating category:", error);
// Show error message
Swal.fire({
icon: "error",
title: "Error!",
text: error.message || "Failed to update category. Please try again.",
});
}
};
return (
<div>
{/* Edit Category */}
<div className="modal fade" id="edit-category">
<div className="modal-dialog modal-dialog-centered custom-modal-two">
<div className="modal-content">
<div className="page-wrapper-new p-0">
<div className="content">
<div className="modal-header">
<div className="page-title">
<h4>Edit Category</h4>
</div>
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div className="modal-body custom-modal-body">
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Category</label>
<input
type="text"
className="form-control border"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="mb-3">
<label className="form-label">Description</label>
<input
type="text"
className="form-control border"
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">
<span className="status-label">Status</span>
<input
type="checkbox"
id="user3"
className="check"
defaultChecked="true"
/>
<label htmlFor="user3" className="checktoggle" />
</div>
</div>
<div className="modal-footer-btn">
<button
type="button"
className="btn btn-outline-dark me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<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>
</div>
</div>
</div>
</div>
</div>
{/* /Edit Category */}
</div>
);
};
export default EditCategoryList;

View File

@ -1,26 +1,149 @@
import React from "react"; import { useEffect, useState } from "react";
import Select from "react-select"; import { useDispatch, useSelector } from "react-redux";
import ImageWithBasePath from "../../img/imagewithbasebath"; import AsyncSelect from "react-select/async";
import { Link } from "react-router-dom"; import Swal from "sweetalert2";
import outletsApi from "../../../services/outletsApi";
import productsApi from "../../../services/productsApi";
import {
createInventory,
fetchInventories,
updateInventory,
} from "../../redux/actions/inventoryActions";
const ManageStockModal = () => { const ManageStockModal = () => {
const options1 = [ const dispatch = useDispatch();
{ value: "choose", label: "Choose" }, const { creating, updating, currentInventory } = useSelector(
{ value: "lobarHandy", label: "Lobar Handy" }, (state) => state.inventories
{ value: "quaintWarehouse", label: "Quaint Warehouse" }, );
];
const options2 = [ const initializeFormData = () => {
{ value: "choose", label: "Choose" }, return {
{ value: "selosy", label: "Selosy" }, outlet_id: "",
{ value: "logerro", label: "Logerro" }, product_id: "",
]; quantity: "",
min_stock_level: "",
max_stock_level: "",
};
};
const [formData, setFormData] = useState(initializeFormData());
const [selectedOutlet, setSelectedOutlet] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(null);
const handleInputChange = (e) => {
setFormData({
...formData,
[e.target.name]: Number(e.target.value),
});
};
const handleSelectChange = (field, selectedOption) => {
setFormData({
...formData,
[field]: selectedOption ? selectedOption.value : "",
});
};
useEffect(() => {
if (currentInventory) {
setFormData(currentInventory);
}
}, [currentInventory]);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await dispatch(createInventory(formData));
await dispatch(fetchInventories());
Swal.fire({
title: "Success!",
text: "Stock created successfully!",
icon: "success",
showConfirmButton: false,
timer: 1500,
}).then(() => {
const closeButton = document.querySelector("#add-units .btn-close");
closeButton.click();
setFormData(initializeFormData());
});
} catch (error) {
console.error("Error creating stock:", error);
// Show error message
Swal.fire({
icon: "error",
title: "Error!",
text: error.message || "Failed to create stock. Please try again.",
});
}
};
const handleUpdate = async (e) => {
e.preventDefault();
try {
await dispatch(updateInventory(currentInventory?.id, formData));
await dispatch(fetchInventories());
Swal.fire({
title: "Success!",
text: "Stock updated successfully!",
icon: "success",
showConfirmButton: false,
timer: 1500,
}).then(() => {
const closeButton = document.querySelector("#edit-units .btn-close");
closeButton.click();
setFormData(initializeFormData());
});
} catch (error) {
console.error("Error updating stock:", error);
// Show error message
Swal.fire({
icon: "error",
title: "Error!",
text: error.message || "Failed to update stock. Please try again.",
});
}
};
const loadOutlets = async (inputValue) => {
try {
const response = await outletsApi.getAllOutlets({ search: inputValue });
const options = response.data.outlets.map((item) => ({
label: item.name,
value: item.id,
}));
return options;
} catch (error) {
console.error("Error fetching options:", error);
return [];
}
};
const loadProducts = async (inputValue) => {
try {
const response = await productsApi.getAllProducts({ search: inputValue });
const options = response.data.products.map((item) => ({
label: item.name,
value: item.id,
}));
return options;
} catch (error) {
console.error("Error fetching options:", error);
return [];
}
};
const options3 = [
{ value: "choose", label: "Choose" },
{ value: "steven", label: "Steven" },
{ value: "gravely", label: "Gravely" },
];
return ( return (
<> <>
{/* Add Stock */} {/* Add Stock */}
@ -29,65 +152,109 @@ const ManageStockModal = () => {
<div className="modal-content"> <div className="modal-content">
<div className="page-wrapper-new p-0"> <div className="page-wrapper-new p-0">
<div className="content"> <div className="content">
<div className="modal-header border-0 custom-modal-header"> <div className="modal-header">
<div className="page-title"> <div className="page-title">
<h4>Add Stock</h4> <h4>Create Stock</h4>
</div> </div>
<button <button
type="button" type="button"
className="close" className="btn-close"
data-bs-dismiss="modal" data-bs-dismiss="modal"
aria-label="Close" aria-label="Close"
> ></button>
<span aria-hidden="true">×</span>
</button>
</div> </div>
<div className="modal-body custom-modal-body"> <div className="modal-body custom-modal-body">
<form> <form onSubmit={handleSubmit}>
<div className="mb-3">
<label className="form-label">Outlet</label>
<AsyncSelect
className="select"
loadOptions={loadOutlets}
placeholder="Choose"
isClearable
cacheOptions={true}
defaultOptions={true}
value={selectedOutlet}
onChange={(selectedOption) => {
handleSelectChange("outlet_id", selectedOption);
setSelectedOutlet(selectedOption);
}}
/>
</div>
<div className="mb-3">
<label className="form-label">Product</label>
<AsyncSelect
className="select"
loadOptions={loadProducts}
placeholder="Choose"
isClearable
cacheOptions={true}
defaultOptions={true}
value={selectedProduct}
onChange={(selectedOption) => {
handleSelectChange("product_id", selectedOption);
setSelectedProduct(selectedOption);
}}
/>
</div>
<div className="mb-3">
<label className="form-label">Quantity</label>
<input
type="number"
className="form-control border"
name="quantity"
value={formData.quantity}
onChange={handleInputChange}
/>
</div>
<div className="row"> <div className="row">
<div className="col-lg-6"> <div className="col">
<div className="input-blocks"> <label className="form-label">Min Stock</label>
<label>Warehouse</label> <input
<Select className="select" options={options1} /> type="number"
</div> className="form-control border"
name="min_stock_level"
value={formData.min_stock_level}
onChange={handleInputChange}
/>
</div> </div>
<div className="col-lg-6"> <div className="col">
<div className="input-blocks"> <label className="form-label">Max Stock</label>
<label>Shop</label> <input
<Select className="select" options={options2} /> type="number"
</div> className="form-control border"
</div> name="max_stock_level"
<div className="col-lg-12"> value={formData.max_stock_level}
<div className="input-blocks"> onChange={handleInputChange}
<label>Responsible Person</label> />
<Select className="select" options={options3} />
</div>
</div>
<div className="col-lg-12">
<div className="input-blocks search-form mb-0">
<label>Product</label>
<input
type="text"
className="form-control"
placeholder="Select Product"
/>
<i
data-feather="search"
className="feather-search custom-search"
/>
</div>
</div> </div>
</div> </div>
<div className="modal-footer-btn"> <div className="modal-footer-btn">
<button <button
type="button" type="button"
className="btn btn-cancel me-2" className="btn btn-outline-dark me-2"
data-bs-dismiss="modal" data-bs-dismiss="modal"
> >
Cancel Cancel
</button> </button>
<button type="submit" className="btn btn-submit"> <button
Create 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"
)}
</button> </button>
</div> </div>
</form> </form>
@ -100,167 +267,79 @@ const ManageStockModal = () => {
{/* /Add Stock */} {/* /Add Stock */}
{/* Edit Stock */} {/* Edit Stock */}
<div className="modal fade" id="edit-units"> <div className="modal fade" id="edit-units">
<div className="modal-dialog modal-dialog-centered stock-adjust-modal"> <div className="modal-dialog modal-dialog-centered">
<div className="modal-content"> <div className="modal-content">
<div className="page-wrapper-new p-0"> <div className="page-wrapper-new p-0">
<div className="content"> <div className="content">
<div className="modal-header border-0 custom-modal-header"> <div className="modal-header">
<div className="page-title"> <div className="page-title">
<h4>Edit Stock</h4> <h4>Edit Stock</h4>
</div> </div>
<button <button
type="button" type="button"
className="close" className="btn-close"
data-bs-dismiss="modal" data-bs-dismiss="modal"
aria-label="Close" aria-label="Close"
> ></button>
<span aria-hidden="true">×</span>
</button>
</div> </div>
<div className="modal-body custom-modal-body"> <div className="modal-body custom-modal-body">
<form> <form onSubmit={handleUpdate}>
<div className="input-blocks search-form"> <div className="mb-3">
<label>Product</label> <label className="form-label">Quantity</label>
<input <input
type="text" type="number"
className="form-control" className="form-control border"
defaultValue="Nike Jordan" name="quantity"
/> value={formData.quantity}
<i onChange={handleInputChange}
data-feather="search"
className="feather-search custom-search"
/> />
</div> </div>
<div className="row"> <div className="mb-3">
<div className="col-lg-6"> <label className="form-label">Min Stock</label>
<div className="input-blocks"> <input
<label>Warehouse</label> type="number"
<Select className="select" options={options1} /> className="form-control border"
</div> name="min_stock_level"
</div> value={formData.min_stock_level}
<div className="col-lg-6"> onChange={handleInputChange}
<div className="input-blocks"> />
<label>Shop</label>
<Select className="select" options={options2} />
</div>
</div>
<div className="col-lg-12">
<div className="input-blocks">
<label>Responsible Person</label>
<Select className="select" options={options3} />
</div>
</div>
<div className="col-lg-12">
<div className="input-blocks search-form mb-3">
<label>Product</label>
<input
type="text"
className="form-control"
placeholder="Select Product"
defaultValue="Nike Jordan"
/>
<i
data-feather="search"
className="feather-search custom-search"
/>
</div>
</div>
<div className="col-lg-12">
<div className="modal-body-table">
<div className="table-responsive">
<table className="table datanew">
<thead>
<tr>
<th>Product</th>
<th>SKU</th>
<th>Category</th>
<th>Qty</th>
<th className="no-sort">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div className="productimgname">
<Link
to="#"
className="product-img stock-img"
>
<ImageWithBasePath
src="assets/img/products/stock-img-02.png"
alt="product"
/>
</Link>
<Link to="#">
Nike Jordan
</Link>
</div>
</td>
<td>PT002</td>
<td>Nike</td>
<td>
<div className="product-quantity">
<span className="quantity-btn">
<i
data-feather="minus-circle"
className="feather-search"
/>
</span>
<input
type="text"
className="quntity-input"
defaultValue={2}
/>
<span className="quantity-btn">
+
<i
data-feather="plus-circle"
className="plus-circle"
/>
</span>
</div>
</td>
<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-units"
>
<i
data-feather="edit"
className="feather-edit"
/>
</Link>
<Link
className="confirm-text p-2"
to="#"
>
<i
data-feather="trash-2"
className="feather-trash-2"
/>
</Link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
<div className="mb-3">
<label className="form-label">Max Stock</label>
<input
type="number"
className="form-control border"
name="max_stock_level"
value={formData.max_stock_level}
onChange={handleInputChange}
/>
</div>
<div className="modal-footer-btn"> <div className="modal-footer-btn">
<button <button
type="button" type="button"
className="btn btn-cancel me-2" className="btn btn-outline-dark me-2"
data-bs-dismiss="modal" data-bs-dismiss="modal"
> >
Cancel Cancel
</button> </button>
<button type="submit" className="btn btn-submit"> <button
Save Changes 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"
)}
</button> </button>
</div> </div>
</form> </form>

View File

@ -7,7 +7,6 @@ import { onShowSizeChange } from "./pagination";
const Datatable = ({ props, columns, dataSource }) => { const Datatable = ({ props, columns, dataSource }) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys) => { const onSelectChange = (newSelectedRowKeys) => {
console.log("selectedRowKeys changed: ", selectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys); setSelectedRowKeys(newSelectedRowKeys);
}; };
@ -15,15 +14,18 @@ const Datatable = ({ props, columns, dataSource }) => {
selectedRowKeys, selectedRowKeys,
onChange: onSelectChange, onChange: onSelectChange,
}; };
return ( return (
<Table <Table
key={props} key={props}
className="table datanew dataTable no-footer" className="custom-table table datanew dataTable"
rowSelection={rowSelection} rowSelection={rowSelection}
columns={columns} columns={columns}
dataSource={dataSource} dataSource={dataSource}
rowKey={(record) => record.id} 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,143 @@
import { inventoryApi } from '../../../services/inventoriesApi';
// Action Types
export const INVENTORY_ACTIONS = {
// Fetch Inventories
FETCH_INVENTORIES_REQUEST: 'FETCH_INVENTORIES_REQUEST',
FETCH_INVENTORIES_SUCCESS: 'FETCH_INVENTORIES_SUCCESS',
FETCH_INVENTORIES_FAILURE: 'FETCH_INVENTORIES_FAILURE',
// Fetch Single Inventory
FETCH_INVENTORY_REQUEST: 'FETCH_INVENTORY_REQUEST',
FETCH_INVENTORY_SUCCESS: 'FETCH_INVENTORY_SUCCESS',
FETCH_INVENTORY_FAILURE: 'FETCH_INVENTORY_FAILURE',
// Create Inventory
CREATE_INVENTORY_REQUEST: 'CREATE_INVENTORY_REQUEST',
CREATE_INVENTORY_SUCCESS: 'CREATE_INVENTORY_SUCCESS',
CREATE_INVENTORY_FAILURE: 'CREATE_INVENTORY_FAILURE',
// Update Inventory
UPDATE_INVENTORY_REQUEST: 'UPDATE_INVENTORY_REQUEST',
UPDATE_INVENTORY_SUCCESS: 'UPDATE_INVENTORY_SUCCESS',
UPDATE_INVENTORY_FAILURE: 'UPDATE_INVENTORY_FAILURE',
// Delete Inventory
DELETE_INVENTORY_REQUEST: 'DELETE_INVENTORY_REQUEST',
DELETE_INVENTORY_SUCCESS: 'DELETE_INVENTORY_SUCCESS',
DELETE_INVENTORY_FAILURE: 'DELETE_INVENTORY_FAILURE',
// Search Inventories
SEARCH_INVENTORIES_REQUEST: 'SEARCH_INVENTORIES_REQUEST',
SEARCH_INVENTORIES_SUCCESS: 'SEARCH_INVENTORIES_SUCCESS',
SEARCH_INVENTORIES_FAILURE: 'SEARCH_INVENTORIES_FAILURE',
// Clear States
CLEAR_INVENTORY_ERROR: 'CLEAR_INVENTORY_ERROR',
CLEAR_CURRENT_INVENTORY: 'CLEAR_CURRENT_INVENTORY',
};
// Action Creators
export const fetchInventories = (params = {}) => async (dispatch) => {
dispatch({ type: INVENTORY_ACTIONS.FETCH_INVENTORIES_REQUEST });
try {
const data = await inventoryApi.getAllInventories(params);
dispatch({
type: INVENTORY_ACTIONS.FETCH_INVENTORIES_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: INVENTORY_ACTIONS.FETCH_INVENTORIES_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch inventories',
});
throw error;
}
};
export const fetchInventory = (id) => async (dispatch) => {
dispatch({ type: INVENTORY_ACTIONS.FETCH_INVENTORY_REQUEST });
try {
const data = await inventoryApi.getInventoryById(id);
dispatch({
type: INVENTORY_ACTIONS.FETCH_INVENTORY_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: INVENTORY_ACTIONS.FETCH_INVENTORY_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch inventory',
});
throw error;
}
};
export const createInventory = (inventoryData) => async (dispatch) => {
dispatch({ type: INVENTORY_ACTIONS.CREATE_INVENTORY_REQUEST });
try {
const data = await inventoryApi.createInventory(inventoryData);
dispatch({
type: INVENTORY_ACTIONS.CREATE_INVENTORY_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: INVENTORY_ACTIONS.CREATE_INVENTORY_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to create inventory',
});
throw error;
}
};
export const updateInventory = (id, inventoryData) => async (dispatch) => {
dispatch({ type: INVENTORY_ACTIONS.UPDATE_INVENTORY_REQUEST });
try {
const data = await inventoryApi.updateInventory(id, inventoryData);
dispatch({
type: INVENTORY_ACTIONS.UPDATE_INVENTORY_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: INVENTORY_ACTIONS.UPDATE_INVENTORY_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to update inventory',
});
throw error;
}
};
export const deleteInventory = (id) => async (dispatch) => {
dispatch({ type: INVENTORY_ACTIONS.DELETE_INVENTORY_REQUEST });
try {
await inventoryApi.deleteInventory(id);
dispatch({
type: INVENTORY_ACTIONS.DELETE_INVENTORY_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: INVENTORY_ACTIONS.DELETE_INVENTORY_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to delete inventory',
});
throw error;
}
};
export const clearInventoryError = () => ({
type: INVENTORY_ACTIONS.CLEAR_INVENTORY_ERROR,
});
export const clearCurrentInventory = () => ({
type: INVENTORY_ACTIONS.CLEAR_CURRENT_INVENTORY,
});

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

@ -0,0 +1,132 @@
import { organizationsApi } from '../../../services/organizationsApi';
// Action Types
export const ORGANIZATION_ACTIONS = {
FETCH_ORGANIZATIONS_REQUEST: 'FETCH_ORGANIZATIONS_REQUEST',
FETCH_ORGANIZATIONS_SUCCESS: 'FETCH_ORGANIZATIONS_SUCCESS',
FETCH_ORGANIZATIONS_FAILURE: 'FETCH_ORGANIZATIONS_FAILURE',
FETCH_ORGANIZATION_REQUEST: 'FETCH_ORGANIZATION_REQUEST',
FETCH_ORGANIZATION_SUCCESS: 'FETCH_ORGANIZATION_SUCCESS',
FETCH_ORGANIZATION_FAILURE: 'FETCH_ORGANIZATION_FAILURE',
CREATE_ORGANIZATION_REQUEST: 'CREATE_ORGANIZATION_REQUEST',
CREATE_ORGANIZATION_SUCCESS: 'CREATE_ORGANIZATION_SUCCESS',
CREATE_ORGANIZATION_FAILURE: 'CREATE_ORGANIZATION_FAILURE',
UPDATE_ORGANIZATION_REQUEST: 'UPDATE_ORGANIZATION_REQUEST',
UPDATE_ORGANIZATION_SUCCESS: 'UPDATE_ORGANIZATION_SUCCESS',
UPDATE_ORGANIZATION_FAILURE: 'UPDATE_ORGANIZATION_FAILURE',
DELETE_ORGANIZATION_REQUEST: 'DELETE_ORGANIZATION_REQUEST',
DELETE_ORGANIZATION_SUCCESS: 'DELETE_ORGANIZATION_SUCCESS',
DELETE_ORGANIZATION_FAILURE: 'DELETE_ORGANIZATION_FAILURE',
CLEAR_ORGANIZATION_ERROR: 'CLEAR_ORGANIZATION_ERROR',
CLEAR_CURRENT_ORGANIZATION: 'CLEAR_CURRENT_ORGANIZATION',
};
// Action Creators
export const fetchOrganizations = (params = {}) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_REQUEST });
try {
const data = await organizationsApi.getAllOrganizations(params);
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch organizations',
});
throw error;
}
};
export const fetchOrganization = (id) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_REQUEST });
try {
const data = await organizationsApi.getOrganizationById(id);
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch organization',
});
throw error;
}
};
export const createOrganization = (organizationData) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_REQUEST });
try {
const data = await organizationsApi.createOrganization(organizationData);
dispatch({
type: ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to create organization',
});
throw error;
}
};
export const updateOrganization = (id, organizationData) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_REQUEST });
try {
const data = await organizationsApi.updateOrganization(id, organizationData);
dispatch({
type: ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to update organization',
});
throw error;
}
};
export const deleteOrganization = (id) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_REQUEST });
try {
await organizationsApi.deleteOrganization(id);
dispatch({
type: ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to delete organization',
});
throw error;
}
};
export const clearOrganizationError = () => ({
type: ORGANIZATION_ACTIONS.CLEAR_ORGANIZATION_ERROR,
});
export const clearCurrentOrganization = () => ({
type: ORGANIZATION_ACTIONS.CLEAR_CURRENT_ORGANIZATION,
});

View File

@ -0,0 +1,138 @@
import { outletsApi } from '../../../services/outletsApi';
// Action Types
export const OUTLET_ACTIONS = {
// Fetch Outlets
FETCH_OUTLETS_REQUEST: 'FETCH_OUTLETS_REQUEST',
FETCH_OUTLETS_SUCCESS: 'FETCH_OUTLETS_SUCCESS',
FETCH_OUTLETS_FAILURE: 'FETCH_OUTLETS_FAILURE',
// Fetch Single Outlet
FETCH_OUTLET_REQUEST: 'FETCH_OUTLET_REQUEST',
FETCH_OUTLET_SUCCESS: 'FETCH_OUTLET_SUCCESS',
FETCH_OUTLET_FAILURE: 'FETCH_OUTLET_FAILURE',
// Create Outlet
CREATE_OUTLET_REQUEST: 'CREATE_OUTLET_REQUEST',
CREATE_OUTLET_SUCCESS: 'CREATE_OUTLET_SUCCESS',
CREATE_OUTLET_FAILURE: 'CREATE_OUTLET_FAILURE',
// Update Outlet
UPDATE_OUTLET_REQUEST: 'UPDATE_OUTLET_REQUEST',
UPDATE_OUTLET_SUCCESS: 'UPDATE_OUTLET_SUCCESS',
UPDATE_OUTLET_FAILURE: 'UPDATE_OUTLET_FAILURE',
// Delete Outlet
DELETE_OUTLET_REQUEST: 'DELETE_OUTLET_REQUEST',
DELETE_OUTLET_SUCCESS: 'DELETE_OUTLET_SUCCESS',
DELETE_OUTLET_FAILURE: 'DELETE_OUTLET_FAILURE',
// Clear States
CLEAR_OUTLET_ERROR: 'CLEAR_OUTLET_ERROR',
CLEAR_CURRENT_OUTLET: 'CLEAR_CURRENT_OUTLET',
};
// Action Creators
export const fetchOutlets = (params = {}) => async (dispatch) => {
dispatch({ type: OUTLET_ACTIONS.FETCH_OUTLETS_REQUEST });
try {
const data = await outletsApi.getAllOutlets(params);
dispatch({
type: OUTLET_ACTIONS.FETCH_OUTLETS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: OUTLET_ACTIONS.FETCH_OUTLETS_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch outlets',
});
throw error;
}
};
export const fetchOutlet = (id) => async (dispatch) => {
dispatch({ type: OUTLET_ACTIONS.FETCH_OUTLET_REQUEST });
try {
const data = await outletsApi.getOutletById(id);
dispatch({
type: OUTLET_ACTIONS.FETCH_OUTLET_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: OUTLET_ACTIONS.FETCH_OUTLET_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch outlet',
});
throw error;
}
};
export const createOutlet = (outletData) => async (dispatch) => {
dispatch({ type: OUTLET_ACTIONS.CREATE_OUTLET_REQUEST });
try {
const data = await outletsApi.createOutlet(outletData);
dispatch({
type: OUTLET_ACTIONS.CREATE_OUTLET_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: OUTLET_ACTIONS.CREATE_OUTLET_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to create outlet',
});
throw error;
}
};
export const updateOutlet = (id, outletData) => async (dispatch) => {
dispatch({ type: OUTLET_ACTIONS.UPDATE_OUTLET_REQUEST });
try {
const data = await outletsApi.updateOutlet(id, outletData);
dispatch({
type: OUTLET_ACTIONS.UPDATE_OUTLET_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: OUTLET_ACTIONS.UPDATE_OUTLET_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to update outlet',
});
throw error;
}
};
export const deleteOutlet = (id) => async (dispatch) => {
dispatch({ type: OUTLET_ACTIONS.DELETE_OUTLET_REQUEST });
try {
await outletsApi.deleteOutlet(id);
dispatch({
type: OUTLET_ACTIONS.DELETE_OUTLET_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: OUTLET_ACTIONS.DELETE_OUTLET_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to delete outlet',
});
throw error;
}
};
export const clearOutletError = () => ({
type: OUTLET_ACTIONS.CLEAR_OUTLET_ERROR,
});
export const clearCurrentOutlet = () => ({
type: OUTLET_ACTIONS.CLEAR_CURRENT_OUTLET,
});

View File

@ -0,0 +1,126 @@
import { paymentMethodsApi } from '../../../services/paymentMethodsApi';
export const PAYMENT_METHOD_ACTIONS = {
FETCH_PAYMENT_METHODS_REQUEST: 'FETCH_PAYMENT_METHODS_REQUEST',
FETCH_PAYMENT_METHODS_SUCCESS: 'FETCH_PAYMENT_METHODS_SUCCESS',
FETCH_PAYMENT_METHODS_FAILURE: 'FETCH_PAYMENT_METHODS_FAILURE',
FETCH_PAYMENT_METHOD_REQUEST: 'FETCH_PAYMENT_METHOD_REQUEST',
FETCH_PAYMENT_METHOD_SUCCESS: 'FETCH_PAYMENT_METHOD_SUCCESS',
FETCH_PAYMENT_METHOD_FAILURE: 'FETCH_PAYMENT_METHOD_FAILURE',
CREATE_PAYMENT_METHOD_REQUEST: 'CREATE_PAYMENT_METHOD_REQUEST',
CREATE_PAYMENT_METHOD_SUCCESS: 'CREATE_PAYMENT_METHOD_SUCCESS',
CREATE_PAYMENT_METHOD_FAILURE: 'CREATE_PAYMENT_METHOD_FAILURE',
UPDATE_PAYMENT_METHOD_REQUEST: 'UPDATE_PAYMENT_METHOD_REQUEST',
UPDATE_PAYMENT_METHOD_SUCCESS: 'UPDATE_PAYMENT_METHOD_SUCCESS',
UPDATE_PAYMENT_METHOD_FAILURE: 'UPDATE_PAYMENT_METHOD_FAILURE',
DELETE_PAYMENT_METHOD_REQUEST: 'DELETE_PAYMENT_METHOD_REQUEST',
DELETE_PAYMENT_METHOD_SUCCESS: 'DELETE_PAYMENT_METHOD_SUCCESS',
DELETE_PAYMENT_METHOD_FAILURE: 'DELETE_PAYMENT_METHOD_FAILURE',
CLEAR_PAYMENT_METHOD_ERROR: 'CLEAR_PAYMENT_METHOD_ERROR',
CLEAR_CURRENT_PAYMENT_METHOD: 'CLEAR_CURRENT_PAYMENT_METHOD',
};
// Action Creators
export const fetchPaymentMethods = (params = {}) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_REQUEST });
try {
const data = await paymentMethodsApi.getAll(params);
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const fetchPaymentMethod = (id) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_REQUEST });
try {
const data = await paymentMethodsApi.getById(id);
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const createPaymentMethod = (formData) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_REQUEST });
try {
const data = await paymentMethodsApi.create(formData);
dispatch({
type: PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const updatePaymentMethod = (id, formData) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_REQUEST });
try {
const data = await paymentMethodsApi.update(id, formData);
dispatch({
type: PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const deletePaymentMethod = (id) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_REQUEST });
try {
await paymentMethodsApi.remove(id);
dispatch({
type: PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const clearPaymentMethodError = () => ({
type: PAYMENT_METHOD_ACTIONS.CLEAR_PAYMENT_METHOD_ERROR,
});
export const clearCurrentPaymentMethod = () => ({
type: PAYMENT_METHOD_ACTIONS.CLEAR_CURRENT_PAYMENT_METHOD,
});

View File

@ -89,6 +89,7 @@ export const createProduct = (productData) => async (dispatch) => {
try { try {
const data = await productsApi.createProduct(productData); const data = await productsApi.createProduct(productData);
console.log('data', data)
dispatch({ dispatch({
type: PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS, type: PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS,
payload: data, 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 // Fetch brands
export const fetchBrands = () => async (dispatch) => { export const fetchBrands = () => async (dispatch) => {
try { try {

View File

@ -1,6 +1,13 @@
import { combineReducers } from '@reduxjs/toolkit'; import { combineReducers } from '@reduxjs/toolkit';
import initialState from "./initial.value"; import initialState from "./initial.value";
import productReducer from './reducers/productReducer'; import productReducer from './reducers/productReducer';
import authReducer from './reducers/authReducer';
import categoryReducer from './reducers/categoryReducer';
import orderReducer from './reducers/orderReducer';
import paymentMethodReducer from './reducers/paymentMethodReducer';
import organizationReducer from './reducers/organizationReducer';
import inventoryReducer from './reducers/inventoryReducer';
import outletReducer from './reducers/outletReducer';
// Legacy reducer for existing functionality // Legacy reducer for existing functionality
const legacyReducer = (state = initialState, action) => { const legacyReducer = (state = initialState, action) => {
@ -73,6 +80,13 @@ const legacyReducer = (state = initialState, action) => {
const rootReducer = combineReducers({ const rootReducer = combineReducers({
legacy: legacyReducer, legacy: legacyReducer,
products: productReducer, products: productReducer,
auth: authReducer,
categories: categoryReducer,
orders: orderReducer,
paymentMethods: paymentMethodReducer,
organizations: organizationReducer,
inventories: inventoryReducer,
outlets: outletReducer
}); });
export default rootReducer; 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.data.user;
state.token = action.payload.data.token;
state.loading = false;
state.error = null;
// Store token in localStorage
localStorage.setItem('authToken', action.payload.data.token);
localStorage.setItem('user', JSON.stringify(action.payload.data.user));
},
loginFailure: (state, action) => {
state.loading = false;
state.error = action.payload.error;
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,189 @@
import { INVENTORY_ACTIONS } from '../actions/inventoryActions';
const initialState = {
// Inventory list
inventories: [],
totalInventories: 0,
currentPage: 1,
totalPages: 1,
pageSize: 10,
hasPrevious: false,
hasNext: false,
// Current inventory (for edit/view)
currentInventory: null,
// Search results
searchResults: [],
searchQuery: '',
// Loading states
loading: false,
inventoryLoading: false,
searchLoading: false,
// Error states
error: null,
inventoryError: null,
searchError: null,
// Operation states
creating: false,
updating: false,
deleting: false,
};
const inventoryReducer = (state = initialState, action) => {
switch (action.type) {
// Fetch Inventories
case INVENTORY_ACTIONS.FETCH_INVENTORIES_REQUEST:
return {
...state,
loading: true,
error: null,
};
case INVENTORY_ACTIONS.FETCH_INVENTORIES_SUCCESS: {
const { inventory, total_count, page, total_pages, limit } =
action.payload.data;
return {
...state,
loading: false,
inventories: inventory,
totalInventories: total_count || inventory.length,
currentPage: page || 1,
totalPages: total_pages || 1,
pageSize: limit || 10,
hasPrevious: false,
hasNext: false,
error: null,
};
}
case INVENTORY_ACTIONS.FETCH_INVENTORIES_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
// Fetch Single Inventory
case INVENTORY_ACTIONS.FETCH_INVENTORY_REQUEST:
return {
...state,
inventoryLoading: true,
inventoryError: null,
};
case INVENTORY_ACTIONS.FETCH_INVENTORY_SUCCESS:
return {
...state,
inventoryLoading: false,
currentInventory: action.payload.data,
inventoryError: null,
};
case INVENTORY_ACTIONS.FETCH_INVENTORY_FAILURE:
return {
...state,
inventoryLoading: false,
inventoryError: action.payload,
};
// Create Inventory
case INVENTORY_ACTIONS.CREATE_INVENTORY_REQUEST:
return {
...state,
creating: true,
error: null,
};
case INVENTORY_ACTIONS.CREATE_INVENTORY_SUCCESS:
return {
...state,
creating: false,
inventories: [action.payload.data, ...state.inventories],
totalInventories: state.totalInventories + 1,
error: null,
};
case INVENTORY_ACTIONS.CREATE_INVENTORY_FAILURE:
return {
...state,
creating: false,
error: action.payload,
};
// Update Inventory
case INVENTORY_ACTIONS.UPDATE_INVENTORY_REQUEST:
return {
...state,
updating: true,
error: null,
};
case INVENTORY_ACTIONS.UPDATE_INVENTORY_SUCCESS:
return {
...state,
updating: false,
inventories: state.inventories.map(inv =>
inv.id === action.payload.data.id ? action.payload.data : inv
),
currentInventory: action.payload.data,
error: null,
};
case INVENTORY_ACTIONS.UPDATE_INVENTORY_FAILURE:
return {
...state,
updating: false,
error: action.payload,
};
// Delete Inventory
case INVENTORY_ACTIONS.DELETE_INVENTORY_REQUEST:
return {
...state,
deleting: true,
error: null,
};
case INVENTORY_ACTIONS.DELETE_INVENTORY_SUCCESS:
return {
...state,
deleting: false,
inventories: state.inventories.filter(inv => inv.id !== action.payload),
totalInventories: state.totalInventories - 1,
error: null,
};
case INVENTORY_ACTIONS.DELETE_INVENTORY_FAILURE:
return {
...state,
deleting: false,
error: action.payload,
};
// Clear states
case INVENTORY_ACTIONS.CLEAR_INVENTORY_ERROR:
return {
...state,
error: null,
inventoryError: null,
searchError: null,
};
case INVENTORY_ACTIONS.CLEAR_CURRENT_INVENTORY:
return {
...state,
currentInventory: null,
inventoryError: null,
};
default:
return state;
}
};
export default inventoryReducer;

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

@ -0,0 +1,212 @@
import { ORGANIZATION_ACTIONS } from '../actions/organizationActions';
const initialState = {
// Organizations list
organizations: [],
totalOrganizations: 0,
currentPage: 1,
totalPages: 1,
pageSize: 10,
hasPrevious: false,
hasNext: false,
// Current organization (for edit/view)
currentOrganization: null,
// Search results
searchResults: [],
searchQuery: '',
// Loading states
loading: false,
organizationLoading: false,
searchLoading: false,
// Error states
error: null,
organizationError: null,
searchError: null,
// Operation states
creating: false,
updating: false,
deleting: false,
};
const organizationReducer = (state = initialState, action) => {
switch (action.type) {
// Fetch Organizations
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_REQUEST:
return {
...state,
loading: true,
error: null,
};
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_SUCCESS: {
const { organizations, total_count, page, total_pages, limit } = action.payload.data;
return {
...state,
loading: false,
organizations: organizations,
totalOrganizations: total_count || organizations.length,
currentPage: page || 1,
totalPages: total_pages || 1,
pageSize: limit || 10,
hasPrevious: false,
hasNext: false,
error: null,
};
}
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
// Fetch Single Organization
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_REQUEST:
return {
...state,
organizationLoading: true,
organizationError: null,
};
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_SUCCESS:
return {
...state,
organizationLoading: false,
currentOrganization: action.payload.data,
organizationError: null,
};
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_FAILURE:
return {
...state,
organizationLoading: false,
organizationError: action.payload,
};
// Create Organization
case ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_REQUEST:
return {
...state,
creating: true,
error: null,
};
case ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_SUCCESS:
return {
...state,
creating: false,
organizations: [action.payload.data, ...state.organizations],
totalOrganizations: state.totalOrganizations + 1,
error: null,
};
case ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_FAILURE:
return {
...state,
creating: false,
error: action.payload,
};
// Update Organization
case ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_REQUEST:
return {
...state,
updating: true,
error: null,
};
case ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_SUCCESS:
return {
...state,
updating: false,
organizations: state.organizations.map(org =>
org.id === action.payload.data.id ? action.payload.data : org
),
currentOrganization: action.payload.data,
error: null,
};
case ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_FAILURE:
return {
...state,
updating: false,
error: action.payload,
};
// Delete Organization
case ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_REQUEST:
return {
...state,
deleting: true,
error: null,
};
case ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_SUCCESS:
return {
...state,
deleting: false,
organizations: state.organizations.filter(org => org.id !== action.payload),
totalOrganizations: state.totalOrganizations - 1,
error: null,
};
case ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_FAILURE:
return {
...state,
deleting: false,
error: action.payload,
};
// Search Organizations
case ORGANIZATION_ACTIONS.SEARCH_ORGANIZATIONS_REQUEST:
return {
...state,
searchLoading: true,
searchError: null,
};
case ORGANIZATION_ACTIONS.SEARCH_ORGANIZATIONS_SUCCESS:
return {
...state,
searchLoading: false,
searchResults: action.payload.data || action.payload,
searchQuery: action.payload.query || '',
searchError: null,
};
case ORGANIZATION_ACTIONS.SEARCH_ORGANIZATIONS_FAILURE:
return {
...state,
searchLoading: false,
searchError: action.payload,
};
// Clear States
case ORGANIZATION_ACTIONS.CLEAR_ORGANIZATION_ERROR:
return {
...state,
error: null,
organizationError: null,
searchError: null,
};
case ORGANIZATION_ACTIONS.CLEAR_CURRENT_ORGANIZATION:
return {
...state,
currentOrganization: null,
organizationError: null,
};
default:
return state;
}
};
export default organizationReducer;

View File

@ -0,0 +1,212 @@
import { OUTLET_ACTIONS } from '../actions/outletActions';
const initialState = {
// Outlets list
outlets: [],
totalOutlets: 0,
currentPage: 1,
totalPages: 1,
pageSize: 10,
hasPrevious: false,
hasNext: false,
// Current outlet (for edit/view)
currentOutlet: null,
// Search results
searchResults: [],
searchQuery: '',
// Loading states
loading: false,
outletLoading: false,
searchLoading: false,
// Error states
error: null,
outletError: null,
searchError: null,
// Operation states
creating: false,
updating: false,
deleting: false,
};
const outletReducer = (state = initialState, action) => {
switch (action.type) {
// Fetch Outlets
case OUTLET_ACTIONS.FETCH_OUTLETS_REQUEST:
return {
...state,
loading: true,
error: null,
};
case OUTLET_ACTIONS.FETCH_OUTLETS_SUCCESS: {
const { outlets, total_count, page, total_pages, limit } = action.payload.data;
return {
...state,
loading: false,
outlets: outlets,
totalOutlets: total_count || outlets.length,
currentPage: page || 1,
totalPages: total_pages || 1,
pageSize: limit || 10,
hasPrevious: false,
hasNext: false,
error: null,
};
}
case OUTLET_ACTIONS.FETCH_OUTLETS_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
// Fetch Single Outlet
case OUTLET_ACTIONS.FETCH_OUTLET_REQUEST:
return {
...state,
outletLoading: true,
outletError: null,
};
case OUTLET_ACTIONS.FETCH_OUTLET_SUCCESS:
return {
...state,
outletLoading: false,
currentOutlet: action.payload.data,
outletError: null,
};
case OUTLET_ACTIONS.FETCH_OUTLET_FAILURE:
return {
...state,
outletLoading: false,
outletError: action.payload,
};
// Create Outlet
case OUTLET_ACTIONS.CREATE_OUTLET_REQUEST:
return {
...state,
creating: true,
error: null,
};
case OUTLET_ACTIONS.CREATE_OUTLET_SUCCESS:
return {
...state,
creating: false,
outlets: [action.payload.data, ...state.outlets],
totalOutlets: state.totalOutlets + 1,
error: null,
};
case OUTLET_ACTIONS.CREATE_OUTLET_FAILURE:
return {
...state,
creating: false,
error: action.payload,
};
// Update Outlet
case OUTLET_ACTIONS.UPDATE_OUTLET_REQUEST:
return {
...state,
updating: true,
error: null,
};
case OUTLET_ACTIONS.UPDATE_OUTLET_SUCCESS:
return {
...state,
updating: false,
outlets: state.outlets.map((outlet) =>
outlet.id === action.payload.data.id ? action.payload.data : outlet
),
currentOutlet: action.payload.data,
error: null,
};
case OUTLET_ACTIONS.UPDATE_OUTLET_FAILURE:
return {
...state,
updating: false,
error: action.payload,
};
// Delete Outlet
case OUTLET_ACTIONS.DELETE_OUTLET_REQUEST:
return {
...state,
deleting: true,
error: null,
};
case OUTLET_ACTIONS.DELETE_OUTLET_SUCCESS:
return {
...state,
deleting: false,
outlets: state.outlets.filter((outlet) => outlet.id !== action.payload),
totalOutlets: state.totalOutlets - 1,
error: null,
};
case OUTLET_ACTIONS.DELETE_OUTLET_FAILURE:
return {
...state,
deleting: false,
error: action.payload,
};
// Search Outlets
case OUTLET_ACTIONS.SEARCH_OUTLETS_REQUEST:
return {
...state,
searchLoading: true,
searchError: null,
};
case OUTLET_ACTIONS.SEARCH_OUTLETS_SUCCESS:
return {
...state,
searchLoading: false,
searchResults: action.payload.data || action.payload,
searchQuery: action.payload.query || '',
searchError: null,
};
case OUTLET_ACTIONS.SEARCH_OUTLETS_FAILURE:
return {
...state,
searchLoading: false,
searchError: action.payload,
};
// Clear States
case OUTLET_ACTIONS.CLEAR_OUTLET_ERROR:
return {
...state,
error: null,
outletError: null,
searchError: null,
};
case OUTLET_ACTIONS.CLEAR_CURRENT_OUTLET:
return {
...state,
currentOutlet: null,
outletError: null,
};
default:
return state;
}
};
export default outletReducer;

View File

@ -0,0 +1,106 @@
import { PAYMENT_METHOD_ACTIONS } from '../actions/paymentMethodActions';
const initialState = {
paymentMethods: [],
totalPaymentMethods: 0,
currentPage: 1,
totalPages: 1,
pageSize: 10,
currentPaymentMethod: null,
loading: false,
creating: false,
updating: false,
deleting: false,
error: null,
detailError: null,
};
const paymentMethodReducer = (state = initialState, action) => {
switch (action.type) {
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_REQUEST:
return { ...state, loading: true, error: null };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_SUCCESS: {
const { payment_methods, total_count, page, total_pages, limit } = action.payload.data;
return {
...state,
loading: false,
paymentMethods: payment_methods,
totalPaymentMethods: total_count || payment_methods.length,
currentPage: page || 1,
totalPages: total_pages || 1,
pageSize: limit || 10,
};
}
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_FAILURE:
return { ...state, loading: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_REQUEST:
return { ...state, detailLoading: true, detailError: null };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_SUCCESS:
return { ...state, detailLoading: false, currentPaymentMethod: action.payload.data };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_FAILURE:
return { ...state, detailLoading: false, detailError: action.payload };
case PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_REQUEST:
return { ...state, creating: true };
case PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_SUCCESS:
return {
...state,
creating: false,
paymentMethods: [action.payload.data, ...state.paymentMethods],
totalPaymentMethods: state.totalPaymentMethods + 1,
};
case PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_FAILURE:
return { ...state, creating: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_REQUEST:
return { ...state, updating: true };
case PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_SUCCESS:
return {
...state,
updating: false,
paymentMethods: state.paymentMethods.map((item) =>
item.id === action.payload.data.id ? action.payload.data : item
),
currentPaymentMethod: action.payload.data,
};
case PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_FAILURE:
return { ...state, updating: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_REQUEST:
return { ...state, deleting: true };
case PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_SUCCESS:
return {
...state,
deleting: false,
paymentMethods: state.paymentMethods.filter((item) => item.id !== action.payload),
totalPaymentMethods: state.totalPaymentMethods - 1,
};
case PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_FAILURE:
return { ...state, deleting: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.CLEAR_PAYMENT_METHOD_ERROR:
return { ...state, error: null, detailError: null };
case PAYMENT_METHOD_ACTIONS.CLEAR_CURRENT_PAYMENT_METHOD:
return { ...state, currentPaymentMethod: null };
default:
return state;
}
};
export default paymentMethodReducer;

View File

@ -1,4 +1,4 @@
import { PRODUCT_ACTIONS } from '../actions/productActions'; import { PRODUCT_ACTIONS } from "../actions/productActions";
const initialState = { const initialState = {
// Products list // Products list
@ -6,7 +6,7 @@ const initialState = {
totalProducts: 0, totalProducts: 0,
currentPage: 1, currentPage: 1,
totalPages: 1, totalPages: 1,
pageSize: 20, pageSize: 10,
hasPrevious: false, hasPrevious: false,
hasNext: false, hasNext: false,
@ -15,7 +15,7 @@ const initialState = {
// Search results // Search results
searchResults: [], searchResults: [],
searchQuery: '', searchQuery: "",
// Categories and brands // Categories and brands
categories: [], categories: [],
@ -49,20 +49,19 @@ const productReducer = (state = initialState, action) => {
case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS: { case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS: {
// Handle different API response structures // Handle different API response structures
const isArrayResponse = Array.isArray(action.payload); const { products, total_count, page, total_pages, limit } =
const products = isArrayResponse ? action.payload : (action.payload.data || action.payload.items || []); action.payload.data;
const pagination = action.payload.pagination || {};
return { return {
...state, ...state,
loading: false, loading: false,
products: products, products: products,
totalProducts: pagination.totalCount || action.payload.total || products.length, totalProducts: total_count,
currentPage: pagination.currentPage || action.payload.currentPage || 1, currentPage: page,
totalPages: pagination.totalPages || action.payload.totalPages || 1, totalPages: total_pages,
pageSize: pagination.pageSize || 20, pageSize: limit,
hasPrevious: pagination.hasPrevious || false, hasPrevious: false,
hasNext: pagination.hasNext || false, hasNext: false,
error: null, error: null,
}; };
} }
@ -86,7 +85,7 @@ const productReducer = (state = initialState, action) => {
return { return {
...state, ...state,
productLoading: false, productLoading: false,
currentProduct: action.payload, currentProduct: action.payload.data,
productError: null, productError: null,
}; };
@ -106,10 +105,11 @@ const productReducer = (state = initialState, action) => {
}; };
case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS: case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS:
console.log("state", state);
return { return {
...state, ...state,
creating: false, creating: false,
products: [action.payload, ...state.products],
totalProducts: state.totalProducts + 1, totalProducts: state.totalProducts + 1,
error: null, error: null,
}; };
@ -133,7 +133,7 @@ const productReducer = (state = initialState, action) => {
return { return {
...state, ...state,
updating: false, updating: false,
products: state.products.map(product => products: state.products.map((product) =>
product.id === action.payload.id ? action.payload.data : product product.id === action.payload.id ? action.payload.data : product
), ),
currentProduct: action.payload.data, currentProduct: action.payload.data,
@ -159,7 +159,9 @@ const productReducer = (state = initialState, action) => {
return { return {
...state, ...state,
deleting: false, deleting: false,
products: state.products.filter(product => product.id !== action.payload), products: state.products.filter(
(product) => product.id !== action.payload
),
totalProducts: state.totalProducts - 1, totalProducts: state.totalProducts - 1,
error: null, error: null,
}; };
@ -184,7 +186,7 @@ const productReducer = (state = initialState, action) => {
...state, ...state,
searchLoading: false, searchLoading: false,
searchResults: action.payload.data || action.payload, searchResults: action.payload.data || action.payload,
searchQuery: action.payload.query || '', searchQuery: action.payload.query || "",
searchError: null, searchError: null,
}; };

View File

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

View File

@ -0,0 +1,387 @@
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 { deletePaymentMethod, fetchPaymentMethod, fetchPaymentMethods } from "../../core/redux/actions/paymentMethodActions";
import { formatDate } from "../../utils/date";
const PaymentMethodList = () => {
const {
paymentMethods: apiPaymentMethods,
loading,
error,
totalPaymentMethods,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.paymentMethods);
const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header);
const dataSource = apiPaymentMethods?.length > 0 ? apiPaymentMethods : [];
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadPaymentMethods = async () => {
try {
const searchParams = {
page: currentPage,
limit: pageSize,
search: debouncedSearchTerm || "",
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== "")
);
await dispatch(fetchPaymentMethods(cleanParams));
} catch (error) {
console.error("Failed to load categories", error);
}
};
loadPaymentMethods();
}, [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 = totalPaymentMethods || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
const handleDeletePaymentMethod = async (paymentMethodId) => {
try {
await dispatch(deletePaymentMethod(paymentMethodId));
// Show success message
MySwal.fire({
title: "Deleted!",
text: "Payment Method has been deleted successfully.",
icon: "success",
className: "btn btn-success",
customClass: {
confirmButton: "btn btn-success",
},
});
} catch (error) {
console.error("Failed to delete payment method:", error);
MySwal.fire({
title: "Error!",
text: "Failed to delete payment method. Please try again.",
icon: "error",
className: "btn btn-danger",
customClass: {
confirmButton: "btn btn-danger",
},
});
}
};
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<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: "Payment Method",
dataIndex: "paymentmethod",
render: (_, record) => {
return <span>{record.name}</span>;
},
sorter: (a, b) => a.name.length - b.name.length,
},
{
title: "Type",
dataIndex: "type",
render: (_, record) => {
return <span>{record?.type}</span>;
},
sorter: (a, b) => a.type.length - b.type.length,
},
{
title: "Created On",
dataIndex: "createdon",
render: (_, record) => {
return <span>{formatDate(record.created_at)}</span>;
},
sorter: (a, b) => a.created_at.length - b.created_at.length,
},
{
title: "Status",
dataIndex: "status",
render: (_, record) => (
<Tag color="#87d068">{record.is_active ? "Active" : "Inactive"}</Tag>
),
sorter: (a, b) => a.status.length - b.status.length,
},
{
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"
onClick={() => dispatch(fetchPaymentMethod(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<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) {
handleDeletePaymentMethod(record.id || record.key);
}
});
}}
>
<Trash2 className="feather-trash-2" />
</Link>
</div>
</td>
),
},
];
const MySwal = withReactContent(Swal);
return (
<div>
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Payment Method</h4>
<h6>Manage your payment methods</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<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"
/>
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
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>
<Select
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
<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(fetchPaymentMethods())}
>
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="paymentmethod-list-pagination"
/>
</>
)}
</div>
</div>
</div>
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
);
};
export default PaymentMethodList;

File diff suppressed because it is too large Load Diff

View File

@ -1,324 +1,388 @@
import React, { useState } from 'react' import { Select, Tag } from "antd";
import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import {
import ImageWithBasePath from '../../core/img/imagewithbasebath'; ChevronUp,
import { Link } from 'react-router-dom'; PlusCircle,
import { ChevronUp, Filter, PlusCircle, RotateCcw, Sliders, StopCircle, Zap } from 'feather-icons-react/build/IconComponents'; RotateCcw,
import { useDispatch, useSelector } from 'react-redux'; Trash2,
import { setToogleHeader } from '../../core/redux/action'; } from "feather-icons-react/build/IconComponents";
import Select from 'react-select'; import { useEffect, useState } from "react";
import { DatePicker } from 'antd'; import { OverlayTrigger, Tooltip } from "react-bootstrap";
import AddCategoryList from '../../core/modals/inventory/addcategorylist'; import { useDispatch, useSelector } from "react-redux";
import EditCategoryList from '../../core/modals/inventory/editcategorylist'; import { Link } from "react-router-dom";
import withReactContent from 'sweetalert2-react-content'; import Swal from "sweetalert2";
import Swal from 'sweetalert2'; import withReactContent from "sweetalert2-react-content";
import Table from '../../core/pagination/datatable' 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 CategoryList = () => {
const {
categories: apiCategories,
loading,
error,
totalCategories,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.categories);
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header); 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 [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const toggleFilterVisibility = () => { const [pageSize, setPageSize] = useState(reduxPageSize || 10);
setIsFilterVisible((prevVisibility) => !prevVisibility); const [searchTerm, setSearchTerm] = useState("");
};
const [selectedDate, setSelectedDate] = useState(new Date()); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const handleDateChange = (date) => {
setSelectedDate(date); 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 !== "")
);
await dispatch(fetchCategories(cleanParams));
} catch (error) {
console.error("Failed to load categories", error);
}
}; };
loadCategories();
}, [dispatch, currentPage, pageSize, debouncedSearchTerm]);
const oldandlatestvalue = [ // Debounce search term
{ value: 'date', label: 'Sort by Date' }, useEffect(() => {
{ value: 'newest', label: 'Newest' }, const timer = setTimeout(() => {
{ value: 'oldest', label: 'Oldest' }, setDebouncedSearchTerm(searchTerm);
]; }, 500); // 500ms delay
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' },
];
const renderTooltip = (props) => ( return () => clearTimeout(timer);
<Tooltip id="pdf-tooltip" {...props}> }, [searchTerm]);
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
)
const columns = [ // Handle pagination
const handlePageChange = (page) => {
setCurrentPage(page);
};
{ // Handle page size change
title: "Category", const handlePageSizeChange = (newPageSize) => {
dataIndex: "category", setPageSize(newPageSize);
sorter: (a, b) => a.category.length - b.category.length, 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",
}, },
{ });
title: "Category Slug", } catch (error) {
dataIndex: "categoryslug", console.error("Failed to delete category:", error);
sorter: (a, b) => a.categoryslug.length - b.categoryslug.length, MySwal.fire({
title: "Error!",
text: "Failed to delete category. Please try again.",
icon: "error",
className: "btn btn-danger",
customClass: {
confirmButton: "btn btn-danger",
}, },
{ });
title: "Created On", }
dataIndex: "createdon", };
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>
),
sorter: (a, b) => a.status.length - b.status.length,
},
{
title: 'Actions',
dataIndex: 'actions',
key: 'actions',
render: () => (
<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">
<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>
</div>
</td>
)
},
]
const MySwal = withReactContent(Swal);
const showConfirmationAlert = () => { const renderTooltip = (props) => (
MySwal.fire({ <Tooltip id="pdf-tooltip" {...props}>
title: 'Are you sure?', Pdf
text: 'You won\'t be able to revert this!', </Tooltip>
showCancelButton: true, );
confirmButtonColor: '#00ff00', const renderExcelTooltip = (props) => (
confirmButtonText: 'Yes, delete it!', <Tooltip id="excel-tooltip" {...props}>
cancelButtonColor: '#ff0000', Excel
cancelButtonText: 'Cancel', </Tooltip>
}).then((result) => { );
if (result.isConfirmed) { const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<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: () => <Tag color="#87d068">active</Tag>,
sorter: (a, b) => a.status.length - b.status.length,
},
{
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"
onClick={() => dispatch(fetchCategory(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<Link
className="confirm-text p-2"
to="#"
onClick={(e) => {
e.preventDefault();
MySwal.fire({ MySwal.fire({
title: 'Deleted!', title: "Are you sure?",
text: 'Your file has been deleted.', text: "You won't be able to revert this!",
className: "btn btn-success", showCancelButton: true,
confirmButtonText: 'OK', confirmButtonColor: "#00ff00",
customClass: { confirmButtonText: "Yes, delete it!",
confirmButton: 'btn btn-success', cancelButtonColor: "#ff0000",
}, cancelButtonText: "Cancel",
}).then((result) => {
if (result.isConfirmed) {
handleDeleteCategory(record.id || record.key);
}
}); });
} else { }}
MySwal.close(); >
} <Trash2 className="feather-trash-2" />
</Link>
}); </div>
}; </td>
return ( ),
<div> },
<div className="page-wrapper"> ];
<div className="content"> const MySwal = withReactContent(Swal);
<div className="page-header">
<div className="add-item d-flex"> return (
<div className="page-title"> <div>
<h4>Category</h4> <div className="page-wrapper">
<h6>Manage your categories</h6> <div className="content">
</div> <div className="page-header">
</div> <div className="add-item d-flex">
<ul className="table-top-head"> <div className="page-title">
<li> <h4>Category</h4>
<OverlayTrigger placement="top" overlay={renderTooltip}> <h6>Manage your categories</h6>
<Link> </div>
<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" />
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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)) }}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
type="text"
placeholder="Search"
className="form-control form-control-sm formsearch"
/>
<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"
/>
</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">
<Table columns={columns} dataSource={dataSource} />
</div>
</div>
</div>
{/* /product list */}
</div>
</div> </div>
<AddCategoryList /> <ul className="table-top-head">
<EditCategoryList /> <li>
</div> <OverlayTrigger placement="top" overlay={renderTooltip}>
) <Link>
} <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"
/>
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
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>
export default CategoryList <Select
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
<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>
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
);
};
export default CategoryList;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,388 @@
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 OutletList = () => {
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 = apiCategories?.length > 0 ? apiCategories : [];
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 !== "")
);
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}>
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<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: () => <Tag color="#87d068">active</Tag>,
sorter: (a, b) => a.status.length - b.status.length,
},
{
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"
onClick={() => dispatch(fetchCategory(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<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);
return (
<div>
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Category</h4>
<h6>Manage your categories</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<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"
/>
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
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>
<Select
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
<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>
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
);
};
export default OutletList;

View File

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

View File

@ -1,36 +1,33 @@
import { Select, Space } from "antd";
import { import {
Box,
ChevronUp, ChevronUp,
Edit, Edit,
Eye, Eye,
Filter,
GitMerge,
PlusCircle, PlusCircle,
RotateCcw, RotateCcw,
Sliders,
StopCircle,
Trash2, Trash2,
} from "feather-icons-react/build/IconComponents"; } 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 { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom"; 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 ImageWithBasePath from "../../core/img/imagewithbasebath";
import Brand from "../../core/modals/inventory/brand"; 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 Table from "../../core/pagination/datatable";
import { setToogleHeader } from "../../core/redux/action"; import { setToogleHeader } from "../../core/redux/action";
import { Download } from "react-feather";
import { import {
fetchProducts, clearProductError,
fetchProduct,
deleteProduct, deleteProduct,
clearProductError fetchProduct,
fetchProducts,
} from "../../core/redux/actions/productActions"; } 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 // Add CSS animations for beautiful UI
const shimmerKeyframes = ` const shimmerKeyframes = `
@ -62,11 +59,16 @@ const shimmerKeyframes = `
`; `;
// Inject CSS into head if not already present // Inject CSS into head if not already present
if (typeof document !== 'undefined' && !document.getElementById('beautiful-pagination-styles')) { if (
const styleSheet = document.createElement('style'); typeof document !== "undefined" &&
styleSheet.id = 'beautiful-pagination-styles'; !document.getElementById("beautiful-pagination-styles")
styleSheet.type = 'text/css'; ) {
styleSheet.innerText = shimmerKeyframes + ` const styleSheet = document.createElement("style");
styleSheet.id = "beautiful-pagination-styles";
styleSheet.type = "text/css";
styleSheet.innerText =
shimmerKeyframes +
`
/* Hide all Ant Design pagination elements */ /* Hide all Ant Design pagination elements */
.ant-pagination, .ant-pagination,
.ant-pagination-item, .ant-pagination-item,
@ -433,40 +435,24 @@ const ProductList = () => {
totalProducts, totalProducts,
totalPages, totalPages,
pageSize: reduxPageSize, pageSize: reduxPageSize,
currentPage: reduxCurrentPage currentPage: reduxCurrentPage,
} = useSelector((state) => state.products); } = useSelector((state) => state.products);
// Fallback to legacy data if API data is not available const dataSource = apiProducts?.length > 0 ? apiProducts : [];
const legacyProducts = useSelector((state) => state.legacy?.product_list || []);
const dataSource = apiProducts.length > 0 ? apiProducts : legacyProducts;
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = useSelector((state) => state.legacy?.toggle_header || false); const data = useSelector((state) => state.legacy?.toggle_header || false);
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
// State for pagination - sync with Redux // State for pagination - sync with Redux
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1); const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 20); const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
// State for filter values const [category, setCategory] = useState(null);
const [filterValues, setFilterValues] = useState({
product: '',
category: '',
subCategory: '',
brand: '',
priceRange: ''
});
// Debounced search term // Debounced search term
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const toggleFilterVisibility = () => { const [categoryOptions, setCategoryOptions] = useState([]);
setIsFilterVisible((prevVisibility) => !prevVisibility);
};
const route = all_routes; const route = all_routes;
@ -479,29 +465,51 @@ const ProductList = () => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [searchTerm]); }, [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 // Fetch products when debounced search term or pagination changes
useEffect(() => { useEffect(() => {
const loadProducts = async () => { const loadProducts = async () => {
try { try {
const searchParams = { const searchParams = {
Page: currentPage, page: currentPage,
PageSize: pageSize, limit: pageSize,
SearchTerm: debouncedSearchTerm || '' search: debouncedSearchTerm || "",
category_id: category,
}; };
// Remove empty parameters // Remove empty parameters
const cleanParams = Object.fromEntries( const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== '') Object.entries(searchParams).filter(([, value]) => value !== "")
); );
await dispatch(fetchProducts(cleanParams)); await dispatch(fetchProducts(cleanParams));
} catch (error) { } catch (error) {
console.error('Failed to load products:', error); console.error("Failed to load products:", error);
} }
}; };
loadProducts(); loadProducts();
}, [dispatch, currentPage, pageSize, debouncedSearchTerm]); }, [dispatch, currentPage, pageSize, debouncedSearchTerm, category]);
// Handle product deletion // Handle product deletion
const handleDeleteProduct = async (productId) => { const handleDeleteProduct = async (productId) => {
@ -518,7 +526,7 @@ const ProductList = () => {
}, },
}); });
} catch (error) { } catch (error) {
console.error('Failed to delete product:', error); console.error("Failed to delete product:", error);
MySwal.fire({ MySwal.fire({
title: "Error!", title: "Error!",
text: "Failed to delete product. Please try again.", text: "Failed to delete product. Please try again.",
@ -542,74 +550,12 @@ const ProductList = () => {
// Handle pagination // Handle pagination
const handlePageChange = (page) => { const handlePageChange = (page) => {
setCurrentPage(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 // Handle page size change
const handlePageSizeChange = (newPageSize) => { const handlePageSizeChange = (newPageSize) => {
setPageSize(newPageSize); setPageSize(newPageSize);
setCurrentPage(1); // Reset to first page when changing page size 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 // Calculate pagination info
@ -617,85 +563,95 @@ const ProductList = () => {
const calculatedTotalPages = Math.ceil(totalRecords / pageSize); const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages; const actualTotalPages = totalPages || calculatedTotalPages;
// Debug logs removed for production
// Clear error when component unmounts // Clear error when component unmounts
useEffect(() => { useEffect(() => {
return () => { return () => {
dispatch(clearProductError()); dispatch(clearProductError());
}; };
}, [dispatch]); }, [dispatch]);
const options = [ const options = [
{ value: "sortByDate", label: "Sort by Date" }, { label: "Sort By: Last 7 Days", value: "last7days" },
{ value: "140923", label: "14 09 23" }, { label: "Sort By: Last Month", value: "lastmonth" },
{ value: "110923", label: "11 09 23" }, { 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 = [ const columns = [
{ {
title: "Sản phẩm", title: "SKU",
dataIndex: "sku",
render: (_, record) => {
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 || "";
return skuA.length - skuB.length;
},
},
{
title: "Product",
dataIndex: "product", dataIndex: "product",
render: (text, record) => ( render: (text, record) => (
<span className="productimgname"> <span className="productimgname">
<Link to="/profile" className="product-img stock-img"> <Link className="product-img stock-img">
<ImageWithBasePath <ImageWithBasePath
alt={record.name || text || "Product"} alt={"jpg"}
src={record.productImage || record.image || record.img} src={record.image_url}
/> />
</Link> </Link>
<Link to="/profile">{text}</Link> <Link className="fw-medium text-black">{record.name}</Link>
</span> </span>
), ),
sorter: (a, b) => a.product.length - b.product.length, sorter: (a, b) => a.product.length - b.product.length,
}, },
{ {
title: "Mã", title: "Category",
dataIndex: "sku",
render: (_, record) => {
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 || '';
return skuA.length - skuB.length;
},
},
{
title: "Danh mục",
dataIndex: "category", dataIndex: "category",
render: (_, record) => { render: (_, record) => {
const category = record.category || record.categoryName || '-'; const category = record.category || record.categoryName || "-";
return <span>{category}</span>; return <span>{category}</span>;
}, },
sorter: (a, b) => { sorter: (a, b) => {
const catA = a.category || a.categoryName || ''; const catA = a.category || a.categoryName || "";
const catB = b.category || b.categoryName || ''; const catB = b.category || b.categoryName || "";
return catA.length - catB.length; return catA.length - catB.length;
}, },
}, },
{ {
title: "Thương hiệu", title: "Business",
dataIndex: "brand", dataIndex: "business",
render: (_, record) => { render: (_, record) => {
const brand = record.brand || record.brandName || '-'; const brand = record.business_type || record.brandName || "-";
return <span>{brand}</span>; return <span>{brand}</span>;
}, },
sorter: (a, b) => { sorter: (a, b) => {
const brandA = a.brand || a.brandName || ''; const brandA = a.brand || a.brandName || "";
const brandB = b.brand || b.brandName || ''; const brandB = b.brand || b.brandName || "";
return brandA.length - brandB.length; return brandA.length - brandB.length;
}, },
}, },
{ {
title: "Giá", title: "Printer",
dataIndex: "printer",
render: (_, record) => {
const printer = record.printer_type || "-";
return <span>{printer}</span>;
},
sorter: (a, b) => {
const brandA = a.brand || a.brandName || "";
const brandB = b.brand || b.brandName || "";
return brandA.length - brandB.length;
},
},
{
title: "Price",
dataIndex: "price", dataIndex: "price",
render: (_, record) => { render: (_, record) => {
const price = record.price || record.salePrice || record.unitPrice || 0; 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) => { sorter: (a, b) => {
const priceA = Number(a.price || a.salePrice || a.unitPrice || 0); const priceA = Number(a.price || a.salePrice || a.unitPrice || 0);
@ -704,54 +660,30 @@ const ProductList = () => {
}, },
}, },
{ {
title: "Đơn vị", title: "Cost",
dataIndex: "unit", dataIndex: "cost",
render: (_, record) => { render: (_, record) => {
const unit = record.unit || record.unitOfMeasure || '-'; const cost = record.cost || 0;
return <span>{unit}</span>; return <span>{formatRupiah(Number(cost))}</span>;
}, },
sorter: (a, b) => { sorter: (a, b) => {
const unitA = a.unit || a.unitOfMeasure || ''; const priceA = Number(a.price || a.salePrice || a.unitPrice || 0);
const unitB = b.unit || b.unitOfMeasure || ''; const priceB = Number(b.price || b.salePrice || b.unitPrice || 0);
return unitA.length - unitB.length; return priceA - priceB;
}, },
}, },
{ {
title: "Qty", title: "Action",
dataIndex: "qty",
render: (_, record) => {
// Try multiple possible field names for quantity
const quantity = record.qty || record.quantity || record.stock || record.stockQuantity || 0;
return <span>{quantity}</span>;
},
sorter: (a, b) => {
const qtyA = a.qty || a.quantity || a.stock || a.stockQuantity || 0;
const qtyB = b.qty || b.quantity || b.stock || b.stockQuantity || 0;
return Number(qtyA) - Number(qtyB);
},
},
{
title: "Người tạo",
dataIndex: "createdby",
render: (text, record) => (
<span className="created-by-text">
<Link to="/profile" style={{ color: '#3498db', textDecoration: 'none' }}>
{record.createdBy || text || "Admin"}
</Link>
</span>
),
sorter: (a, b) => a.createdby.length - b.createdby.length,
},
{
title: "Thao tác",
dataIndex: "action", dataIndex: "action",
render: (text, record) => ( render: (text, record) => (
<td className="action-table-data"> <td className="action-table-data">
<div className="edit-delete-action"> <div className="edit-delete-action">
<div className="input-block add-lists"></div> <div className="input-block add-lists"></div>
<Link className="me-2 p-2" to={route.productdetails}> <Link
<Eye className="feather-view" /> className="me-2 p-2"
to={`${route.productdetails}/${record.id || record.key}`}
>
<Eye className="feather-edit" />
</Link> </Link>
<Link <Link
className="me-2 p-2" className="me-2 p-2"
@ -795,8 +727,6 @@ const ProductList = () => {
]; ];
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
// Removed showConfirmationAlert as we handle confirmation inline
const renderTooltip = (props) => ( const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}> <Tooltip id="pdf-tooltip" {...props}>
Pdf Pdf
@ -822,14 +752,15 @@ const ProductList = () => {
Collapse Collapse
</Tooltip> </Tooltip>
); );
return ( return (
<div className="page-wrapper"> <div className="page-wrapper">
<div className="content"> <div className="content">
<div className="page-header"> <div className="page-header">
<div className="add-item d-flex"> <div className="add-item d-flex">
<div className="page-title"> <div className="page-title">
<h4>Danh sách sản phẩm</h4> <h4>Product List</h4>
<h6>Quản sản phẩm</h6> <h6>Manage your products</h6>
</div> </div>
</div> </div>
<ul className="table-top-head"> <ul className="table-top-head">
@ -884,18 +815,17 @@ const ProductList = () => {
<div className="page-btn"> <div className="page-btn">
<Link to={route.addproduct} className="btn btn-added"> <Link to={route.addproduct} className="btn btn-added">
<PlusCircle className="me-2 iconsize" /> <PlusCircle className="me-2 iconsize" />
Thêm mới Add New Product
</Link> </Link>
</div> </div>
<div className="page-btn import"> <div className="page-btn import">
<Link <Link
to="#" className="btn btn-outline-primary rounded-2"
className="btn btn-added color"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#view-notes" data-bs-target="#view-notes"
> >
<Download className="me-2" /> <Download className="me-2 iconsize" />
Nhập sản phẩm Import Product
</Link> </Link>
</div> </div>
</div> </div>
@ -917,291 +847,28 @@ const ProductList = () => {
</Link> </Link>
</div> </div>
</div> </div>
<div className="search-path">
<Link <Space warp>
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 <Select
className="select" style={{ height: 36, width: 100 }}
options={options} placeholder={"Category"}
placeholder="14 09 23" options={categoryOptions}
value={
categoryOptions.find(
(option) => option.value === category
) || null
}
onChange={(selectedOption) => setCategory(selectedOption)}
/> />
</div>
<Select
style={{ height: 36 }}
defaultValue={options[0]?.value}
options={options}
/>
</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"> <div className="table-responsive">
{loading ? ( {loading ? (
<div className="text-center p-4"> <div className="text-center p-4">

View File

@ -1,16 +1,48 @@
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 ImageWithBasePath from "../../../core/img/imagewithbasebath";
import { Link } from "react-router-dom";
import { all_routes } from "../../../Router/all_routes"; import { all_routes } from "../../../Router/all_routes";
import authApi from "../../../services/authApi";
const Signin = () => { const Signin = () => {
const route = all_routes; 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 ( return (
<div className="main-wrapper"> <div className="main-wrapper">
<div className="account-content"> <div className="account-content">
<div className="login-wrapper bg-img"> <div className="login-wrapper bg-img">
<div className="login-content"> <div className="login-content">
<form action="index"> <form onSubmit={handleSubmit}>
<div className="login-userset"> <div className="login-userset">
<div className="login-logo logo-normal"> <div className="login-logo logo-normal">
<ImageWithBasePath src="assets/img/logo.png" alt="img" /> <ImageWithBasePath src="assets/img/logo.png" alt="img" />
@ -24,10 +56,22 @@ const Signin = () => {
Access the Dreamspos panel using your email and passcode. Access the Dreamspos panel using your email and passcode.
</h4> </h4>
</div> </div>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<div className="form-login mb-3"> <div className="form-login mb-3">
<label className="form-label">Email Address</label> <label className="form-label">Email Address</label>
<div className="form-addons"> <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 <ImageWithBasePath
src="assets/img/icons/mail.svg" src="assets/img/icons/mail.svg"
alt="img" alt="img"
@ -39,7 +83,11 @@ const Signin = () => {
<div className="pass-group"> <div className="pass-group">
<input <input
type="password" type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
className="pass-input form-control" className="pass-input form-control"
required
/> />
<span className="fas toggle-password fa-eye-slash" /> <span className="fas toggle-password fa-eye-slash" />
</div> </div>
@ -63,9 +111,13 @@ const Signin = () => {
</div> </div>
</div> </div>
<div className="form-login"> <div className="form-login">
<Link to={route.dashboard} className="btn btn-login"> <button
Sign In type="submit"
</Link> className="btn btn-login"
disabled={authState.loading}
>
{authState.loading ? 'Signing In...' : 'Sign In'}
</button>
</div> </div>
<div className="signinform"> <div className="signinform">
<h4> <h4>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,115 +1,181 @@
import React, { useState } from "react"; import { Tag } from "antd";
import Breadcrumbs from "../../core/breadcrumbs"; import { useEffect, useState } from "react";
import { Filter, Sliders } from "react-feather";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import Select from "react-select";
import { Link } from "react-router-dom";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import { Archive, Box, Calendar, User } from "react-feather";
import ManageStockModal from "../../core/modals/stocks/managestockModal";
import { Edit, Trash2 } from "react-feather"; import { Edit, Trash2 } from "react-feather";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import CustomPagination from "../../components/CustomPagination";
import Breadcrumbs from "../../core/breadcrumbs";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import ManageStockModal from "../../core/modals/stocks/managestockModal";
import Table from "../../core/pagination/datatable"; import Table from "../../core/pagination/datatable";
import { useSelector } from "react-redux"; import {
deleteInventory,
fetchInventories,
INVENTORY_ACTIONS,
} from "../../core/redux/actions/inventoryActions";
import { formatDate } from "../../utils/date";
const Managestock = () => { const Managestock = () => {
const data = useSelector((state) => state.managestockdata); const {
inventories: apiInventories,
loading,
error,
totalInventories,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.inventories);
const [isFilterVisible, setIsFilterVisible] = useState(false); const dispatch = useDispatch();
const [selectedDate, setSelectedDate] = useState(null); const dataSource = apiInventories?.length > 0 ? apiInventories : [];
const toggleFilterVisibility = () => { const [params, setParams] = useState({
setIsFilterVisible((prevVisibility) => !prevVisibility); page: reduxCurrentPage || 1,
}; limit: reduxPageSize || 10,
const handleDateChange = (date) => { });
setSelectedDate(date); const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadInventories = async () => {
try {
const receivedParams = {
page: params.page,
limit: params.limit,
search: debouncedSearchTerm || "",
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(receivedParams).filter(([, value]) => value !== "")
);
await dispatch(fetchInventories(cleanParams));
} catch (error) {
console.error("Failed to fetch orders", error);
}
};
loadInventories();
}, [dispatch, params, debouncedSearchTerm]);
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
handleSetParams("page", 1);
}, 500); // 500ms delay
return () => clearTimeout(timer);
}, [searchTerm]);
const handleSetParams = (key, value) => {
setParams({
...params,
[key]: value,
});
}; };
const options = [ const handleSearch = (e) => {
{ value: "sortByDate", label: "Sort by Date" }, const value = e.target.value;
{ value: "140923", label: "14 09 23" }, setSearchTerm(value);
{ value: "110923", label: "11 09 23" }, };
];
const warehouseOptions = [ // Handle pagination
{ value: "Choose Warehouse", label: "Choose Warehouse" }, const handlePageChange = (page) => {
{ value: "Lobar Handy", label: "Lobar Handy" }, handleSetParams("page", page);
{ value: "Quaint Warehouse", label: "Quaint Warehouse" }, };
{ value: "Traditional Warehouse", label: "Traditional Warehouse" },
{ value: "Cool Warehouse", label: "Cool Warehouse" },
];
const productOptions = [ // Handle page size change
{ value: "Choose Product", label: "Choose Product" }, const handlePageSizeChange = (newPageSize) => {
{ value: "Nike Jordan", label: "Nike Jordan" }, handleSetParams("limit", newPageSize);
{ value: "Apple Series 5 Watch", label: "Apple Series 5 Watch" }, handleSetParams("page", 1);
{ value: "Amazon Echo Dot", label: "Amazon Echo Dot" }, };
{ value: "Lobar Handy", label: "Lobar Handy" },
];
const personOptions = [ const handleDelete = async (id) => {
{ value: "Choose Person", label: "Choose Person" }, try {
{ value: "Steven", label: "Steven" }, await dispatch(deleteInventory(id));
{ value: "Gravely", label: "Gravely" },
]; MySwal.fire({
title: "Deleted!",
text: "Your stock has been deleted.",
className: "btn btn-success",
confirmButtonText: "OK",
customClass: {
confirmButton: "btn btn-success",
},
});
} catch (error) {
console.error("Failed to delete inventory", error);
}
};
// Calculate pagination info
const totalRecords = totalInventories || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / params.limit);
const actualTotalPages = totalPages || calculatedTotalPages;
const columns = [ const columns = [
{ {
title: "Warehouse", title: "Outlet",
dataIndex: "Warehouse", dataIndex: "outlet",
render: (_, record) => <span>{record.outlet || "-"}</span>,
sorter: (a, b) => a.Warehouse.length - b.Warehouse.length, sorter: (a, b) => a.Warehouse.length - b.Warehouse.length,
}, },
{
title: "Shop",
dataIndex: "Shop",
sorter: (a, b) => a.Shop.length - b.Shop.length,
},
{ {
title: "Product", title: "Product",
dataIndex: "Product", dataIndex: "Product",
render: (text, record) => ( render: (text, record) => (
<span className="userimgname"> <span className="userimgname">
<Link to="#" className="product-img"> <Link to="#" className="product-img">
<ImageWithBasePath alt="img" src={record.Product.Image} /> <ImageWithBasePath alt="img" src={record.product_image_url || ""} />
</Link> </Link>
<Link to="#">{record.Product.Name}</Link> <Link to="#">{record.product_name || ""}</Link>
</span> </span>
), ),
sorter: (a, b) => a.Product.Name.length - b.Product.Name.length, sorter: (a, b) => a.Product.Name.length - b.Product.Name.length,
}, },
{ {
title: "Date", title: "Qty",
dataIndex: "Date", dataIndex: "qty",
render: (_, record) => <span>{record.quantity || 0}</span>,
sorter: (a, b) => a.Email.length - b.Email.length, sorter: (a, b) => a.Email.length - b.Email.length,
}, },
{ {
title: "Person", title: "Status",
dataIndex: "Person", dataIndex: "status",
render: (text, record) => ( render: (_, record) => (
<span className="userimgname"> <Tag color={record.is_low_stock ? "red" : "green"}>
<Link to="#" className="product-img"> {record.is_low_stock ? "Low Stock" : "Normal Stock"}
<ImageWithBasePath alt="img" src={record.Person.Image} /> </Tag>
</Link>
<Link to="#">{record.Person.Name}</Link>
</span>
), ),
sorter: (a, b) => a.Person.Name.length - b.Person.Name.length, sorter: (a, b) => a.Person.Name.length - b.Person.Name.length,
}, },
{ {
title: "Quantity", title: "Record Level",
dataIndex: "Quantity", dataIndex: "recordlevel",
render: (_, record) => <span>{record.record_level || "-"}</span>,
sorter: (a, b) => a.Quantity.length - b.Quantity.length,
},
{
title: "Updated Date",
dataIndex: "updateddate",
render: (_, record) => (
<span>{formatDate(record.updated_at) || "-"}</span>
),
sorter: (a, b) => a.Quantity.length - b.Quantity.length, sorter: (a, b) => a.Quantity.length - b.Quantity.length,
}, },
{ {
title: "Action", title: "Action",
dataIndex: "action", dataIndex: "action",
render: () => ( render: (_, record) => (
<td className="action-table-data"> <td className="action-table-data">
<div className="edit-delete-action"> <div className="edit-delete-action">
<div className="input-block add-lists"></div> <div className="input-block add-lists"></div>
@ -119,6 +185,12 @@ const Managestock = () => {
to="#" to="#"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#edit-units" data-bs-target="#edit-units"
onClick={() =>
dispatch({
type: INVENTORY_ACTIONS.FETCH_INVENTORY_SUCCESS,
payload: { data: record },
})
}
> >
<Edit className="feather-edit" /> <Edit className="feather-edit" />
</Link> </Link>
@ -126,7 +198,7 @@ const Managestock = () => {
<Link <Link
className="confirm-text p-2" className="confirm-text p-2"
to="#" to="#"
onClick={showConfirmationAlert} onClick={() => showConfirmationAlert(record.id)}
> >
<Trash2 className="feather-trash-2" /> <Trash2 className="feather-trash-2" />
</Link> </Link>
@ -139,7 +211,7 @@ const Managestock = () => {
const MySwal = withReactContent(Swal); const MySwal = withReactContent(Swal);
const showConfirmationAlert = () => { const showConfirmationAlert = (id) => {
MySwal.fire({ MySwal.fire({
title: "Are you sure?", title: "Are you sure?",
text: "You won't be able to revert this!", text: "You won't be able to revert this!",
@ -150,20 +222,13 @@ const Managestock = () => {
cancelButtonText: "Cancel", cancelButtonText: "Cancel",
}).then((result) => { }).then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
MySwal.fire({ handleDelete(id);
title: "Deleted!",
text: "Your file has been deleted.",
className: "btn btn-success",
confirmButtonText: "OK",
customClass: {
confirmButton: "btn btn-success",
},
});
} else { } else {
MySwal.close(); MySwal.close();
} }
}); });
}; };
return ( return (
<div className="page-wrapper"> <div className="page-wrapper">
<div className="content"> <div className="content">
@ -182,117 +247,58 @@ const Managestock = () => {
type="text" type="text"
placeholder="Search" placeholder="Search"
className="form-control form-control-sm formsearch" className="form-control form-control-sm formsearch"
value={searchTerm}
onChange={handleSearch}
/> />
<Link to className="btn btn-searchset"> <Link to className="btn btn-searchset">
<i data-feather="search" className="feather-search" /> <i data-feather="search" className="feather-search" />
</Link> </Link>
</div> </div>
</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 stylewidth">
<Sliders className="info-img" />
<Select
className="select "
options={options}
placeholder="Sort by Date"
/>
</div>
</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-2 col-sm-6 col-12">
<div className="input-blocks">
<Archive className="info-img" />
<Select
className="select"
options={warehouseOptions}
placeholder="Choose Warehouse"
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<Box className="info-img" />
<Select
className="select"
options={productOptions}
placeholder="Choose Product"
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<Calendar className="info-img" />
<div className="input-groupicon">
<DatePicker
selected={selectedDate}
onChange={handleDateChange}
dateFormat="dd/MM/yyyy"
placeholderText="Choose Date"
className="datetimepicker"
/>
</div>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<User className="info-img" />
<Select
className="select"
options={personOptions}
placeholder="Choose Person"
/>
</div>
</div>
<div className="col-lg-4 col-sm-6 col-12 ms-auto">
<div className="input-blocks">
<a className="btn btn-filters ms-auto">
{" "}
<i
data-feather="search"
className="feather-search"
/>{" "}
Search{" "}
</a>
</div>
</div>
</div>
</div>
</div>
{/* /Filter */}
<div className="table-responsive"> <div className="table-responsive">
<Table {loading ? (
className="table datanew" <div className="text-center p-4">
columns={columns} <div className="spinner-border text-primary" role="status">
dataSource={data} <span className="visually-hidden">Loading...</span>
rowKey={(record) => record.id} </div>
// pagination={true} <p className="mt-2">Loading products...</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(fetchInventories())}
>
Retry
</button>
</div>
) : (
<>
<Table
className="table datanew"
columns={columns}
dataSource={dataSource}
rowKey={(record) => record.id}
/>
<CustomPagination
currentPage={params.page}
pageSize={params.limit}
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> </div>
</div> </div>

View File

@ -1,118 +1,153 @@
import React, { useState } from "react"; import { useEffect, useState } from "react";
import Breadcrumbs from "../../core/breadcrumbs";
import { Filter, Sliders } from "react-feather";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import Select from "react-select";
import { Link } from "react-router-dom";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import { Archive, Box, Calendar, User, Edit, Trash2 } from "react-feather"; import { Edit, Trash2 } from "react-feather";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import Table from "../../core/pagination/datatable"; import CustomPagination from "../../components/CustomPagination";
import Breadcrumbs from "../../core/breadcrumbs";
import ImageWithBasePath from "../../core/img/imagewithbasebath";
import StockadjustmentModal from "../../core/modals/stocks/stockadjustmentModal"; import StockadjustmentModal from "../../core/modals/stocks/stockadjustmentModal";
import { useSelector } from "react-redux"; import Table from "../../core/pagination/datatable";
import { fetchInventories } from "../../core/redux/actions/inventoryActions";
import { formatDate } from "../../utils/date";
import { Tag } from "antd";
const StockAdjustment = () => { const StockAdjustment = () => {
const data = useSelector((state) => state.managestockdata); const {
inventories: apiInventories,
loading,
error,
totalInventories,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.inventories);
const [isFilterVisible, setIsFilterVisible] = useState(false); const dispatch = useDispatch();
const [selectedDate, setSelectedDate] = useState(null); const dataSource = apiInventories?.length > 0 ? apiInventories : [];
const toggleFilterVisibility = () => { const [params, setParams] = useState({
setIsFilterVisible((prevVisibility) => !prevVisibility); page: reduxCurrentPage || 1,
limit: reduxPageSize || 10,
});
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadInventories = async () => {
try {
const receivedParams = {
page: params.page,
limit: params.limit,
search: debouncedSearchTerm || "",
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(receivedParams).filter(([, value]) => value !== "")
);
await dispatch(fetchInventories(cleanParams));
} catch (error) {
console.error("Failed to fetch orders", error);
}
};
loadInventories();
}, [dispatch, params, debouncedSearchTerm]);
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
handleSetParams("page", 1);
}, 500); // 500ms delay
return () => clearTimeout(timer);
}, [searchTerm]);
const handleSetParams = (key, value) => {
setParams({
...params,
[key]: value,
});
}; };
const handleDateChange = (date) => { const handleSearch = (e) => {
setSelectedDate(date); const value = e.target.value;
setSearchTerm(value);
}; };
const options = [
{ value: "sortByDate", label: "Sort by Date" },
{ value: "140923", label: "14 09 23" },
{ value: "110923", label: "11 09 23" },
];
const warehouseOptions = [ // Handle pagination
{ value: "Choose Warehouse", label: "Choose Warehouse" }, const handlePageChange = (page) => {
{ value: "Lobar Handy", label: "Lobar Handy" }, handleSetParams("page", page);
{ value: "Quaint Warehouse", label: "Quaint Warehouse" }, };
{ value: "Traditional Warehouse", label: "Traditional Warehouse" },
{ value: "Cool Warehouse", label: "Cool Warehouse" },
];
const productOptions = [ // Handle page size change
{ value: "Choose Product", label: "Choose Product" }, const handlePageSizeChange = (newPageSize) => {
{ value: "Nike Jordan", label: "Nike Jordan" }, handleSetParams("limit", newPageSize);
{ value: "Apple Series 5 Watch", label: "Apple Series 5 Watch" }, handleSetParams("page", 1);
{ value: "Amazon Echo Dot", label: "Amazon Echo Dot" }, };
{ value: "Lobar Handy", label: "Lobar Handy" },
];
const personOptions = [ // Calculate pagination info
{ value: "Choose Person", label: "Choose Person" }, const totalRecords = totalInventories || dataSource.length;
{ value: "Steven", label: "Steven" }, const calculatedTotalPages = Math.ceil(totalRecords / params.limit);
{ value: "Gravely", label: "Gravely" }, const actualTotalPages = totalPages || calculatedTotalPages;
];
const columns = [ const columns = [
{ {
title: "Warehouse", title: "Outlet",
dataIndex: "Warehouse", dataIndex: "outlet",
render: (_, record) => <span>{record.outlet || "-"}</span>,
sorter: (a, b) => a.Warehouse.length - b.Warehouse.length, sorter: (a, b) => a.Warehouse.length - b.Warehouse.length,
}, },
{
title: "Shop",
dataIndex: "Shop",
sorter: (a, b) => a.Shop.length - b.Shop.length,
},
{ {
title: "Product", title: "Product",
dataIndex: "Product", dataIndex: "Product",
render: (text, record) => ( render: (text, record) => (
<span className="userimgname"> <span className="userimgname">
<Link to="#" className="product-img"> <Link to="#" className="product-img">
<ImageWithBasePath alt="img" src={record.Product.Image} /> <ImageWithBasePath alt="img" src={record.product_image_url || ""} />
</Link> </Link>
<Link to="#">{record.Product.Name}</Link> <Link to="#">{record.product_name || ""}</Link>
</span> </span>
), ),
sorter: (a, b) => a.Product.Name.length - b.Product.Name.length, sorter: (a, b) => a.Product.Name.length - b.Product.Name.length,
}, },
{ {
title: "Date", title: "Qty",
dataIndex: "Date", dataIndex: "qty",
render: (_, record) => <span>{record.quantity || 0}</span>,
sorter: (a, b) => a.Email.length - b.Email.length, sorter: (a, b) => a.Email.length - b.Email.length,
}, },
{ {
title: "Person", title: "Status",
dataIndex: "Person", dataIndex: "status",
render: (text, record) => ( render: (_, record) => (
<span className="userimgname"> <Tag color={record.is_low_stock ? "red" : "green"}>
<Link to="#" className="product-img"> {record.is_low_stock ? "Low Stock" : "Normal Stock"}
<ImageWithBasePath alt="img" src={record.Person.Image} /> </Tag>
</Link>
<Link to="#">{record.Person.Name}</Link>
</span>
), ),
sorter: (a, b) => a.Person.Name.length - b.Person.Name.length, sorter: (a, b) => a.Person.Name.length - b.Person.Name.length,
}, },
{ {
title: "Notes", title: "Record Level",
// dataIndex: "Quantity", dataIndex: "recordlevel",
render: () => ( render: (_, record) => <span>{record.record_level || "-"}</span>,
<Link sorter: (a, b) => a.Quantity.length - b.Quantity.length,
to="#" },
className="view-note" {
data-bs-toggle="modal" title: "Updated Date",
data-bs-target="#view-notes" dataIndex: "updateddate",
> render: (_, record) => (
View Note <span>{formatDate(record.updated_at) || "-"}</span>
</Link>
), ),
sorter: (a, b) => a.Notes.length - b.Notes.length, sorter: (a, b) => a.Quantity.length - b.Quantity.length,
}, },
{ {
@ -191,115 +226,58 @@ const StockAdjustment = () => {
type="text" type="text"
placeholder="Search" placeholder="Search"
className="form-control form-control-sm formsearch" className="form-control form-control-sm formsearch"
name="search"
value={searchTerm}
onChange={handleSearch}
/> />
<Link to className="btn btn-searchset"> <Link to className="btn btn-searchset">
<i data-feather="search" className="feather-search" /> <i data-feather="search" className="feather-search" />
</Link> </Link>
</div> </div>
</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 stylewidth">
<Sliders className="info-img" />
<Select
className="select "
options={options}
placeholder="Sort by Date"
/>
</div>
</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-2 col-sm-6 col-12">
<div className="input-blocks">
<Archive className="info-img" />
<Select
className="select"
options={warehouseOptions}
placeholder="Choose Warehouse"
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<Box className="info-img" />
<Select
className="select"
options={productOptions}
placeholder="Choose Product"
/>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<Calendar className="info-img" />
<div className="input-groupicon">
<DatePicker
selected={selectedDate}
onChange={handleDateChange}
dateFormat="dd/MM/yyyy"
placeholderText="Choose Date"
className="datetimepicker"
/>
</div>
</div>
</div>
<div className="col-lg-2 col-sm-6 col-12">
<div className="input-blocks">
<User className="info-img" />
<Select
className="select"
options={personOptions}
placeholder="Choose Person"
/>
</div>
</div>
<div className="col-lg-4 col-sm-6 col-12 ms-auto">
<div className="input-blocks">
<a className="btn btn-filters ms-auto">
{" "}
<i
data-feather="search"
className="feather-search"
/>{" "}
Search{" "}
</a>
</div>
</div>
</div>
</div>
</div>
{/* /Filter */}
<div className="table-responsive"> <div className="table-responsive">
<Table {loading ? (
className="table datanew" <div className="text-center p-4">
columns={columns} <div className="spinner-border text-primary" role="status">
dataSource={data} <span className="visually-hidden">Loading...</span>
/> </div>
<p className="mt-2">Loading products...</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(fetchInventories())}
>
Retry
</button>
</div>
) : (
<>
<Table
className="table datanew"
columns={columns}
dataSource={dataSource}
/>
<CustomPagination
currentPage={params.page}
pageSize={params.limit}
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> </div>
</div> </div>

View File

@ -0,0 +1,396 @@
import { Select } 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 { fetchOrganizations } from "../../core/redux/actions/organizationActions";
import { deletePaymentMethod, fetchPaymentMethod, fetchPaymentMethods } from "../../core/redux/actions/paymentMethodActions";
import { formatDate } from "../../utils/date";
const CompanyList = () => {
const {
organizations: apiPaymentMethods,
loading,
error,
totalPaymentMethods,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.organizations);
const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header);
const dataSource = apiPaymentMethods?.length > 0 ? apiPaymentMethods : [];
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadPaymentMethods = async () => {
try {
const searchParams = {
page: currentPage,
limit: pageSize,
search: debouncedSearchTerm || "",
};
// Remove empty parameters
const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== "")
);
await dispatch(fetchOrganizations(cleanParams));
} catch (error) {
console.error("Failed to load categories", error);
}
};
loadPaymentMethods();
}, [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 = totalPaymentMethods || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
const handleDeletePaymentMethod = async (paymentMethodId) => {
try {
await dispatch(deletePaymentMethod(paymentMethodId));
// Show success message
MySwal.fire({
title: "Deleted!",
text: "Payment Method has been deleted successfully.",
icon: "success",
className: "btn btn-success",
customClass: {
confirmButton: "btn btn-success",
},
});
} catch (error) {
console.error("Failed to delete payment method:", error);
MySwal.fire({
title: "Error!",
text: "Failed to delete payment method. Please try again.",
icon: "error",
className: "btn btn-danger",
customClass: {
confirmButton: "btn btn-danger",
},
});
}
};
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<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: "Company Name",
dataIndex: "companyname",
render: (_, record) => {
return <span>{record.name}</span>;
},
sorter: (a, b) => a.name.length - b.name.length,
},
{
title: "Email",
dataIndex: "email",
render: (_, record) => {
return <span>{record?.email ?? '-'}</span>;
},
sorter: (a, b) => a.email.length - b.email.length,
},
{
title: "Phone Number",
dataIndex: "phonenumber",
render: (_, record) => {
return <span>{record?.phone_number ?? '-'}</span>;
},
sorter: (a, b) => a.phone_number.length - b.phone_number.length,
},
{
title: "Plan",
dataIndex: "plan",
render: (_, record) => {
return <span>{record?.plan_type ?? '-'}</span>;
},
sorter: (a, b) => a.plan_type.length - b.plan_type.length,
},
{
title: "Created On",
dataIndex: "createdon",
render: (_, record) => {
return <span>{formatDate(record.created_at)}</span>;
},
sorter: (a, b) => a.created_at.length - b.created_at.length,
},
{
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"
onClick={() => dispatch(fetchPaymentMethod(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<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) {
handleDeletePaymentMethod(record.id || record.key);
}
});
}}
>
<Trash2 className="feather-trash-2" />
</Link>
</div>
</td>
),
},
];
const MySwal = withReactContent(Swal);
return (
<div>
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Companies</h4>
<h6>Manage your companies</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<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"
/>
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
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>
<Select
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
<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(fetchPaymentMethods())}
>
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="paymentmethod-list-pagination"
/>
</>
)}
</div>
</div>
</div>
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
);
};
export default CompanyList;

View File

@ -1,354 +1,396 @@
import React, { useState } from 'react' import React, { useEffect, useState } from "react";
import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import ImageWithBasePath from '../../core/img/imagewithbasebath'; import ImageWithBasePath from "../../core/img/imagewithbasebath";
import { ChevronUp, RotateCcw } from 'feather-icons-react/build/IconComponents'; import { ChevronUp, RotateCcw } from "feather-icons-react/build/IconComponents";
import { setToogleHeader } from '../../core/redux/action'; import { setToogleHeader } from "../../core/redux/action";
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from "react-redux";
import { Filter, PlusCircle, Sliders, StopCircle, User, Zap } from 'react-feather'; import {
import Select from 'react-select'; Filter,
import withReactContent from 'sweetalert2-react-content'; PlusCircle,
import Swal from 'sweetalert2'; Sliders,
import Table from '../../core/pagination/datatable' StopCircle,
import AddUsers from '../../core/modals/usermanagement/addusers'; User,
import EditUser from '../../core/modals/usermanagement/edituser'; 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 Users = () => {
const oldandlatestvalue = [
{ 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" },
];
const status = [
{ 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" },
];
const oldandlatestvalue = [ const dispatch = useDispatch();
{ value: 'date', label: 'Sort by Date' }, const data = useSelector((state) => state.toggle_header);
{ value: 'newest', label: 'Newest' }, const [dataSource, setDataSource] = useState([])
{ value: 'oldest', label: 'Oldest' }, const [isFilterVisible, setIsFilterVisible] = useState(false);
]; const toggleFilterVisibility = () => {
const users = [ setIsFilterVisible((prevVisibility) => !prevVisibility);
{ 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' },
];
const role = [
{ value: 'Choose Role', label: 'Choose Role' },
{ value: 'AcStore Keeper', label: 'Store Keeper' },
{ value: 'Salesman', label: 'Salesman' },
];
useEffect(() => {
const loadUsers = async () => {
try {
const response = await usersApi.getAllUsers();
setDataSource(response)
const dispatch = useDispatch(); console.log('response', response)
const data = useSelector((state) => state.toggle_header); } catch (error) {
const dataSource = useSelector((state) => state.userlist_data); console.error("Failed to fetch users", error);
const [isFilterVisible, setIsFilterVisible] = useState(false); }
const toggleFilterVisibility = () => {
setIsFilterVisible((prevVisibility) => !prevVisibility);
}; };
const renderTooltip = (props) => ( loadUsers();
<Tooltip id="pdf-tooltip" {...props}> }, []);
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
)
const columns = [ const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
);
{ const columns = [
title: "User Name", {
dataIndex: "username", title: "User Name",
render: (text, record) => ( dataIndex: "username",
<span className="userimgname"> render: (text, record) => (
<Link to="#" className="userslist-img bg-img"> <span className="userimgname">
<ImageWithBasePath alt="" src={record.img} /> <Link to="#" className="userslist-img bg-img">
</Link> <ImageWithBasePath alt="" src={record.img} />
<div> </Link>
<Link to="#">{text}</Link> <div>
</div> <Link to="#">{text}</Link>
</span> </div>
), </span>
sorter: (a, b) => a.username.length - b.username.length, ),
}, sorter: (a, b) => a.username.length - b.username.length,
},
{ {
title: "Phone", title: "Phone",
dataIndex: "phone", dataIndex: "phone",
sorter: (a, b) => a.phone.length - b.phone.length, sorter: (a, b) => a.phone.length - b.phone.length,
}, },
{ {
title: "Email", title: "Email",
dataIndex: "email", dataIndex: "email",
sorter: (a, b) => a.email.length - b.email.length, sorter: (a, b) => a.email.length - b.email.length,
}, },
{ {
title: "Role", title: "Role",
dataIndex: "role", dataIndex: "role",
sorter: (a, b) => a.role.length - b.role.length, sorter: (a, b) => a.role.length - b.role.length,
}, },
{ {
title: "Created On", title: "Created On",
dataIndex: "createdon", dataIndex: "createdon",
sorter: (a, b) => a.createdon.length - b.createdon.length, sorter: (a, b) => a.createdon.length - b.createdon.length,
}, },
{ {
title: "Status", title: "Status",
dataIndex: "status", dataIndex: "status",
render: (text) => ( render: (text) => (
<div>
{text === "Active" && (
<span className="badge badge-linesuccess">{text}</span>
)}
{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',
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>
</Link>
<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>
</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();
}
});
};
return (
<div> <div>
<div className="page-wrapper"> {text === "Active" && (
<div className="content"> <span className="badge badge-linesuccess">{text}</span>
<div className="page-header"> )}
<div className="add-item d-flex"> {text === "Inactive" && (
<div className="page-title"> <span className="badge badge-linedanger">{text}</span>
<h4>User List</h4> )}
<h6>Manage Your Users</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<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" />
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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)) }}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<a
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-units"
>
<PlusCircle className="me-2" />
Add New User
</a>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
type="text"
placeholder="Search"
className="form-control form-control-sm formsearch"
/>
<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"
/>
</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">
<User className="info-img" />
<Select
className="select"
options={users}
placeholder="Newest"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<StopCircle className="info-img" />
<Select
className="select"
options={status}
placeholder="Choose Status"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<Zap className="info-img" />
<Select
className="select"
options={role}
placeholder="Choose Role"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<a className="btn btn-filters ms-auto">
{" "}
<i data-feather="search" className="feather-search" />{" "}
Search{" "}
</a>
</div>
</div>
</div>
</div>
</div>
{/* /Filter */}
<div className="table-responsive">
<Table columns={columns} dataSource={dataSource} />
</div>
</div>
</div>
{/* /product list */}
</div>
</div>
<AddUsers/>
<EditUser/>
</div> </div>
) ),
} sorter: (a, b) => a.status.length - b.status.length,
},
{
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>
</Link>
<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>
</div>
</td>
),
},
];
const MySwal = withReactContent(Swal);
export default Users 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();
}
});
};
return (
<div>
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>User List</h4>
<h6>Manage Your Users</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<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"
/>
</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>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</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));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<a
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-units"
>
<PlusCircle className="me-2" />
Add New User
</a>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
type="text"
placeholder="Search"
className="form-control form-control-sm formsearch"
/>
<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"
/>
</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">
<User className="info-img" />
<Select
className="select"
options={users}
placeholder="Newest"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<StopCircle className="info-img" />
<Select
className="select"
options={status}
placeholder="Choose Status"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<Zap className="info-img" />
<Select
className="select"
options={role}
placeholder="Choose Role"
/>
</div>
</div>
<div className="col-lg-3 col-sm-6 col-12">
<div className="input-blocks">
<a className="btn btn-filters ms-auto">
{" "}
<i
data-feather="search"
className="feather-search"
/>{" "}
Search{" "}
</a>
</div>
</div>
</div>
</div>
</div>
{/* /Filter */}
<div className="table-responsive">
<Table columns={columns} dataSource={dataSource} />
</div>
</div>
</div>
{/* /product list */}
</div>
</div>
<AddUsers />
<EditUser />
</div>
);
};
export default Users;

View File

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

View File

@ -41,6 +41,7 @@ api.interceptors.response.use(
// Unauthorized - redirect to login or refresh token // Unauthorized - redirect to login or refresh token
localStorage.removeItem('authToken'); localStorage.removeItem('authToken');
// You can add redirect logic here // You can add redirect logic here
window.location.href = '/signin';
} else if (error.response?.status === 500) { } else if (error.response?.status === 500) {
// Server error // Server error
console.error('Server Error:', error.response.data); 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,102 @@
import api from './api';
// Categories API endpoints
const ENDPOINTS = {
CATEGORIES: 'categories',
CATEGORY_BY_ID: (id) => `categories/${id}`,
CATEGORY_PRODUCTS: (id) => `categories/${id}/products`,
};
// 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;
}
},
// 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;

35
src/services/filesApi.js Normal file
View File

@ -0,0 +1,35 @@
import api from "./api";
// Files API endpoints
const ENDPOINTS = {
FILE_UPLOAD: "files/upload",
FILE_BY_ID: (id) => `files/${id}`,
};
// Files API service
export const filesApi = {
// Get all categories
uploadFile: async (fileData) => {
try {
const response = await api.post(ENDPOINTS.FILE_UPLOAD, fileData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data;
} catch (error) {
console.error("Error fetching categories:", error);
throw error;
}
},
getFileById: async (id) => {
try {
const response = await api.get(ENDPOINTS.FILE_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching file ${id}:`, error);
throw error;
}
},
};

View File

@ -0,0 +1,60 @@
import api from './api';
const ENDPOINTS = {
INVENTORIES: 'inventory',
INVENTORY_BY_ID: (id) => `inventory/${id}`,
};
export const inventoryApi = {
getAllInventories: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.INVENTORIES, { params });
return response.data;
} catch (error) {
console.error('Error fetching inventories:', error);
throw error;
}
},
getInventoryById: async (id) => {
try {
const response = await api.get(ENDPOINTS.INVENTORY_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching inventory ${id}:`, error);
throw error;
}
},
createInventory: async (inventoryData) => {
try {
const response = await api.post(ENDPOINTS.INVENTORIES, inventoryData);
return response.data;
} catch (error) {
console.error('Error creating inventory:', error);
throw error;
}
},
updateInventory: async (id, inventoryData) => {
try {
const response = await api.put(ENDPOINTS.INVENTORY_BY_ID(id), inventoryData);
return response.data;
} catch (error) {
console.error(`Error updating inventory ${id}:`, error);
throw error;
}
},
deleteInventory: async (id) => {
try {
const response = await api.delete(ENDPOINTS.INVENTORY_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting inventory ${id}:`, error);
throw error;
}
},
};
export default inventoryApi;

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

@ -0,0 +1,91 @@
import api from './api';
const ENDPOINTS = {
ORGANIZATIONS: 'organizations',
ORGANIZATION_BY_ID: (id) => `organizations/${id}`,
};
export const organizationsApi = {
// Get all organizations
getAllOrganizations: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.ORGANIZATIONS, { params });
return response.data;
} catch (error) {
console.error('Error fetching organizations:', error);
throw error;
}
},
// Get organization by ID
getOrganizationById: async (id) => {
try {
const response = await api.get(ENDPOINTS.ORGANIZATION_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching organization ${id}:`, error);
throw error;
}
},
// Create organization
createOrganization: async (data) => {
try {
const response = await api.post(ENDPOINTS.ORGANIZATIONS, data);
return response.data;
} catch (error) {
console.error('Error creating organization:', error);
throw error;
}
},
// Update organization
updateOrganization: async (id, data) => {
try {
const response = await api.put(ENDPOINTS.ORGANIZATION_BY_ID(id), data);
return response.data;
} catch (error) {
console.error(`Error updating organization ${id}:`, error);
throw error;
}
},
// Delete organization
deleteOrganization: async (id) => {
try {
const response = await api.delete(ENDPOINTS.ORGANIZATION_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting organization ${id}:`, error);
throw error;
}
},
// Bulk update
bulkUpdate: async (organizations) => {
try {
const response = await api.put(`${ENDPOINTS.ORGANIZATIONS}/bulk`, {
organizations,
});
return response.data;
} catch (error) {
console.error('Error bulk updating organizations:', error);
throw error;
}
},
// Bulk delete
bulkDelete: async (ids) => {
try {
const response = await api.delete(`${ENDPOINTS.ORGANIZATIONS}/bulk`, {
data: { ids },
});
return response.data;
} catch (error) {
console.error('Error bulk deleting organizations:', error);
throw error;
}
},
};
export default organizationsApi;

102
src/services/outletsApi.js Normal file
View File

@ -0,0 +1,102 @@
import api from './api';
// Outlets API endpoints
const ENDPOINTS = {
OUTLETS: 'outlets/list',
OUTLET_BY_ID: (id) => `outlets/${id}`,
OUTLET_PRODUCTS: (id) => `outlets/${id}/products`,
};
// Outlets API service
export const outletsApi = {
// Get all outlets
getAllOutlets: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.OUTLETS, { params });
return response.data;
} catch (error) {
console.error('Error fetching outlets:', error);
throw error;
}
},
// Get outlet by ID
getOutletById: async (id) => {
try {
const response = await api.get(ENDPOINTS.OUTLET_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching outlet ${id}:`, error);
throw error;
}
},
// Create new outlet
createOutlet: async (outletData) => {
try {
const response = await api.post(ENDPOINTS.OUTLETS, outletData);
return response.data;
} catch (error) {
console.error('Error creating outlet:', error);
throw error;
}
},
// Update outlet
updateOutlet: async (id, outletData) => {
try {
const response = await api.put(ENDPOINTS.OUTLET_BY_ID(id), outletData);
return response.data;
} catch (error) {
console.error(`Error updating outlet ${id}:`, error);
throw error;
}
},
// Delete outlet
deleteOutlet: async (id) => {
try {
const response = await api.delete(ENDPOINTS.OUTLET_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting outlet ${id}:`, error);
throw error;
}
},
// Get products by outlet
getOutletProducts: async (id, params = {}) => {
try {
const response = await api.get(ENDPOINTS.OUTLET_PRODUCTS(id), { params });
return response.data;
} catch (error) {
console.error(`Error fetching products for outlet ${id}:`, error);
throw error;
}
},
// Bulk operations
bulkUpdateOutlets: async (outlets) => {
try {
const response = await api.put(`${ENDPOINTS.OUTLETS}/bulk`, { outlets });
return response.data;
} catch (error) {
console.error('Error bulk updating outlets:', error);
throw error;
}
},
bulkDeleteOutlets: async (outletIds) => {
try {
const response = await api.delete(`${ENDPOINTS.OUTLETS}/bulk`, {
data: { ids: outletIds }
});
return response.data;
} catch (error) {
console.error('Error bulk deleting outlets:', error);
throw error;
}
},
};
export default outletsApi;

View File

@ -0,0 +1,91 @@
import api from './api';
const ENDPOINTS = {
PAYMENT_METHODS: 'payment-methods',
PAYMENT_METHOD_BY_ID: (id) => `payment-methods/${id}`,
};
export const paymentMethodsApi = {
// Get all payment methods
getAll: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.PAYMENT_METHODS, { params });
return response.data;
} catch (error) {
console.error('Error fetching payment methods:', error);
throw error;
}
},
// Get payment method by ID
getById: async (id) => {
try {
const response = await api.get(ENDPOINTS.PAYMENT_METHOD_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching payment method ${id}:`, error);
throw error;
}
},
// Create new payment method
create: async (data) => {
try {
const response = await api.post(ENDPOINTS.PAYMENT_METHODS, data);
return response.data;
} catch (error) {
console.error('Error creating payment method:', error);
throw error;
}
},
// Update payment method
update: async (id, data) => {
try {
const response = await api.put(ENDPOINTS.PAYMENT_METHOD_BY_ID(id), data);
return response.data;
} catch (error) {
console.error(`Error updating payment method ${id}:`, error);
throw error;
}
},
// Delete payment method
remove: async (id) => {
try {
const response = await api.delete(ENDPOINTS.PAYMENT_METHOD_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting payment method ${id}:`, error);
throw error;
}
},
// Bulk update
bulkUpdate: async (methods) => {
try {
const response = await api.put(`${ENDPOINTS.PAYMENT_METHODS}/bulk`, {
methods,
});
return response.data;
} catch (error) {
console.error('Error bulk updating payment methods:', error);
throw error;
}
},
// Bulk delete
bulkDelete: async (ids) => {
try {
const response = await api.delete(`${ENDPOINTS.PAYMENT_METHODS}/bulk`, {
data: { ids },
});
return response.data;
} catch (error) {
console.error('Error bulk deleting payment methods:', error);
throw error;
}
},
};
export default paymentMethodsApi;

View File

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

@ -708,4 +708,11 @@ button {
box-shadow: 0 3px 12px rgba($dark, .2); box-shadow: 0 3px 12px rgba($dark, .2);
border-color: $dark; border-color: $dark;
} }
} }
.center-vertical {
position: absolute;
top: 50%;
right: 1%;
transform: translateY(-50%);
}

View File

@ -25,4 +25,9 @@
@include margin-padding(5px, null); @include margin-padding(5px, null);
@include box-shadow(null, 0, 2px, 3px, null, rgb(215, 197, 255)); @include box-shadow(null, 0, 2px, 3px, null, rgb(215, 197, 255));
} }
}
.icon-small {
width: 18px;
height: 18px;
} }

View File

@ -1,171 +1,192 @@
.page-link { .page-link {
color: $text-color; color: $text-color;
background-color: $white; background-color: $white;
border: 1px solid $border-color; border: 1px solid $border-color;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
background-color: $light; background-color: $light;
} }
&:hover { &:hover {
color: $primary; color: $primary;
background-color: $light; background-color: $light;
border-color: $border-color; border-color: $border-color;
} }
} }
.page-item.active .page-link { .page-item.active .page-link {
color: $white; color: $white;
background-color: $primary; background-color: $primary;
border-color: $primary; border-color: $primary;
} }
.disabled>.page-link, .page-link.disabled { .disabled > .page-link,
color: $text-color; .page-link.disabled {
background-color: $white; color: $text-color;
border-color: $border-color; background-color: $white;
opacity: 0.7; border-color: $border-color;
opacity: 0.7;
} }
[dir="rtl"] { [dir="rtl"] {
.pagination{ .pagination {
.page-link{ .page-link {
.bx-chevron-left::before{ .bx-chevron-left::before {
content: "\ea50"; content: "\ea50";
} }
.bx-chevron-right::before{ .bx-chevron-right::before {
content: "\ea4d"; content: "\ea4d";
} }
.ri-arrow-right-s-line:before{ .ri-arrow-right-s-line:before {
content: "\ea64"; content: "\ea64";
} }
.ri-arrow-left-s-line:before{ .ri-arrow-left-s-line:before {
content: "\ea6e"; content: "\ea6e";
} }
}
} }
}
} }
.pagination-style-1 .pagination { .pagination-style-1 .pagination {
.page-item { .page-item {
margin: 0 0.25rem; margin: 0 0.25rem;
.page-link { .page-link {
border: 0; border: 0;
border-radius: $border-radius; border-radius: $border-radius;
font-size: 0.8125rem; font-size: 0.8125rem;
i { i {
font-weight: $font-weight-semibold; font-weight: $font-weight-semibold;
} }
}
&.active {
.page-link {
border-radius: $border-radius;
background-color: $primary;
color: $white;
}
&:hover {
.page-link {
border-radius: $border-radius;
background-color: $primary;
color: $white;
}
}
}
&:hover {
.page-link {
background-color: $light;
color: $primary;
}
}
} }
&.active {
.page-link {
border-radius: $border-radius;
background-color: $primary;
color: $white;
}
&:hover {
.page-link {
border-radius: $border-radius;
background-color: $primary;
color: $white;
}
}
}
&:hover {
.page-link {
background-color: $light;
color: $primary;
}
}
}
} }
.pagination-style-2 .pagination { .pagination-style-2 .pagination {
border-radius: $border-radius; border-radius: $border-radius;
.page-item { .page-item {
margin: 0 0.25rem; margin: 0 0.25rem;
.page-link { .page-link {
border: 0 !important; border: 0 !important;
font-size: 0.8125rem; font-size: 0.8125rem;
}
&.active {
.page-link {
background-color: $white;
color: $primary;
position: relative;
font-weight: bold;
&:before {
position: absolute;
content: "";
inset-block-end: 0;
inset-inline-start: 0;
width: 100%;
height: 0.1rem;
background-color: $primary;
}
}
}
&:hover {
.page-link {
background-color: transparent;
}
}
} }
&.active {
.page-link {
background-color: $white;
color: $primary;
position: relative;
font-weight: bold;
&:before {
position: absolute;
content: "";
inset-block-end: 0;
inset-inline-start: 0;
width: 100%;
height: 0.1rem;
background-color: $primary;
}
}
}
&:hover {
.page-link {
background-color: transparent;
}
}
}
} }
.pagination-style-3 .pagination { .pagination-style-3 .pagination {
border-radius: 50px; border-radius: 50px;
padding: 0.25rem; padding: 0.25rem;
align-items: center; align-items: center;
.page-item { .page-item {
margin: 0 0.25rem; margin: 0 0.25rem;
.page-link { .page-link {
border: 0; border: 0;
border-radius: 50px; border-radius: 50px;
font-size: 0.8125rem; font-size: 0.8125rem;
i { i {
font-weight: $font-weight-semibold; font-weight: $font-weight-semibold;
} }
}
&.active {
.page-link {
background-color: $primary;
color: $white;
}
&:hover {
.page-link {
background-color: $primary;
}
}
}
&:hover {
.page-link {
background-color: $light;
}
}
} }
&.active {
.page-link {
background-color: $primary;
color: $white;
}
&:hover {
.page-link {
background-color: $primary;
}
}
}
&:hover {
.page-link {
background-color: $light;
}
}
}
} }
.pagination-style-4 .pagination { .pagination-style-4 .pagination {
.page-item { .page-item {
.page-link { .page-link {
border: 0 !important; border: 0 !important;
font-size: 0.8125rem; font-size: 0.8125rem;
border-radius: $border-radius; border-radius: $border-radius;
i { i {
font-weight: $font-weight-semibold; font-weight: $font-weight-semibold;
} }
}
&.active {
.page-link {
border: 0;
border-radius: $border-radius;
background-color: $primary;
color: $white;
}
&:hover {
.page-link {
background-color: $primary;
}
}
}
&:hover {
.page-link {
background-color: transparent;
}
}
} }
} &.active {
.page-link {
border: 0;
border-radius: $border-radius;
background-color: $primary;
color: $white;
}
&:hover {
.page-link {
background-color: $primary;
}
}
}
&:hover {
.page-link {
background-color: transparent;
}
}
}
}
.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 { a {
color: $secondary; color: $primary;
font-size: $font-size-14; font-size: $font-size-14;
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
line-height: normal; line-height: normal;
@ -533,10 +533,10 @@ caption {
padding: 0; padding: 0;
} }
.table-top { .table-top {
padding: 24px 24px 0; padding: 18px 18px 0;
} }
.table-responsive { .table-responsive {
padding: 24px; padding: 18px;
border-top: 1px solid $gray-400; border-top: 1px solid $gray-400;
.dataTables_wrapper { .dataTables_wrapper {
border: 0; border: 0;
@ -545,7 +545,7 @@ caption {
} }
.tabs-set { .tabs-set {
.nav-tabs { .nav-tabs {
padding: 24px 24px 0; padding: 18px 18px 0;
margin-bottom: 0; margin-bottom: 0;
} }
} }
@ -674,3 +674,12 @@ 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -256,13 +256,12 @@ table {
} }
} }
.custom-modal-header { .custom-modal-header {
background: $body-bg;
padding: 24px; padding: 24px;
.page-title { .page-title {
h4 { h4 {
font-size: $font-size-18; font-size: $font-size-18;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
color: $secondary; color: $primary;
} }
} }
} }

View File

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

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

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

@ -0,0 +1,17 @@
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
};
const formatInputDate = (date) => {
return new Date(date).toLocaleDateString("en-CA");
};
export { formatDate, formatInputDate };