feat: Add inventory management pages with enhanced features
- Add Nhập kho (Import) page (/product-list-2): * Display product, code, import price, import quantity, import date * Smart date-based CSS styling (green/yellow/blue/red by time) * Import date loads from record.createdDate * Remove all checkboxes and selection features * Custom pagination and search functionality - Add Tồn kho (Inventory) page (/product-list-3): * Display total imported, total exported, current stock by product code * Remove warehouse location column * Smart CSS styling by data type (green/yellow/blue/purple) * Mockup data with 5 sample products * Inventory value calculation (stock × unit price) - Update routing and sidebar: * Add routes for both new pages * Add menu items in inventory section * Archive icon for inventory page - Enhanced UI/UX: * Beautiful badge styling with hover effects * Responsive design and loading states * Vietnamese localization * Hide Ant Design pagination and checkboxes completely
This commit is contained in:
parent
ad6106ff67
commit
1171d7c452
@ -1,6 +1,8 @@
|
|||||||
export const all_routes = {
|
export const all_routes = {
|
||||||
dashboard: "/",
|
dashboard: "/",
|
||||||
productlist: "/product-list",
|
productlist: "/product-list",
|
||||||
|
productlist2: "/product-list-2",
|
||||||
|
productlist3: "/product-list-3",
|
||||||
addproduct: "/add-product",
|
addproduct: "/add-product",
|
||||||
salesdashboard: "/sales-dashboard",
|
salesdashboard: "/sales-dashboard",
|
||||||
brandlist: "/brand-list",
|
brandlist: "/brand-list",
|
||||||
|
|||||||
@ -199,6 +199,8 @@ import ProjectTracker from "../feature-module/projects/projecttracker";
|
|||||||
import CreateProject from "../feature-module/projects/createproject";
|
import CreateProject from "../feature-module/projects/createproject";
|
||||||
import EnhancedLoaders from "../feature-module/uiinterface/enhanced-loaders";
|
import EnhancedLoaders from "../feature-module/uiinterface/enhanced-loaders";
|
||||||
import WeddingGuestList from "../feature-module/inventory/weddingGuestList";
|
import WeddingGuestList from "../feature-module/inventory/weddingGuestList";
|
||||||
|
import ProductList2 from "../feature-module/inventory/productlist2";
|
||||||
|
import ProductList3 from "../feature-module/inventory/productlist3";
|
||||||
import { all_routes } from "./all_routes";
|
import { all_routes } from "./all_routes";
|
||||||
export const publicRoutes = [
|
export const publicRoutes = [
|
||||||
{
|
{
|
||||||
@ -215,6 +217,20 @@ export const publicRoutes = [
|
|||||||
element: <ProductList />,
|
element: <ProductList />,
|
||||||
route: Route,
|
route: Route,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 2.1,
|
||||||
|
path: routes.productlist2,
|
||||||
|
name: "products2",
|
||||||
|
element: <ProductList2 />,
|
||||||
|
route: Route,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2.2,
|
||||||
|
path: routes.productlist3,
|
||||||
|
name: "products3",
|
||||||
|
element: <ProductList3 />,
|
||||||
|
route: Route,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
path: routes.addproduct,
|
path: routes.addproduct,
|
||||||
|
|||||||
@ -62,9 +62,11 @@ export const SidebarData = [
|
|||||||
submenuItems: [
|
submenuItems: [
|
||||||
{ label: "Tiến độ dự án", link: "/project-tracker",icon: <Icon.Layers />,showSubRoute: false},
|
{ label: "Tiến độ dự án", link: "/project-tracker",icon: <Icon.Layers />,showSubRoute: false},
|
||||||
{ label: "Sản phẩm", link: "/product-list", icon:<Icon.Box />,showSubRoute: false,submenu: false },
|
{ label: "Sản phẩm", link: "/product-list", icon:<Icon.Box />,showSubRoute: false,submenu: false },
|
||||||
|
{ label: "Nhập kho", link: "/product-list-2", icon:<Icon.Package />,showSubRoute: false,submenu: false },
|
||||||
|
{ label: "Tồn kho", link: "/product-list-3", icon:<Icon.Archive />,showSubRoute: false,submenu: false },
|
||||||
{ label: "Create Product", link: "/add-product", icon: <Icon.PlusSquare />,showSubRoute: false, submenu: false },
|
{ label: "Create Product", link: "/add-product", icon: <Icon.PlusSquare />,showSubRoute: false, submenu: false },
|
||||||
{ label: "Expired Products", link: "/expired-products", icon: <Icon.Codesandbox />,showSubRoute: false,submenu: false },
|
{ label: "Sản phẩm hết hạn", link: "/expired-products", icon: <Icon.Codesandbox />,showSubRoute: false,submenu: false },
|
||||||
{ label: "Low Stocks", link: "/low-stocks", icon: <Icon.TrendingDown />,showSubRoute: false,submenu: false },
|
{ label: "Hàng tồn kho", link: "/low-stocks", icon: <Icon.TrendingDown />,showSubRoute: false,submenu: false },
|
||||||
{ label: "Danh mục", link: "/category-list", icon: <Icon.Codepen />,showSubRoute: false,submenu: false },
|
{ label: "Danh mục", link: "/category-list", icon: <Icon.Codepen />,showSubRoute: false,submenu: false },
|
||||||
{ label: "Sub Category", link: "/sub-categories", icon: <Icon.Speaker />,showSubRoute: false,submenu: false },
|
{ label: "Sub Category", link: "/sub-categories", icon: <Icon.Speaker />,showSubRoute: false,submenu: false },
|
||||||
{ label: "Thương hiệu", link: "/brand-list", icon: <Icon.Tag />,showSubRoute: false,submenu: false },
|
{ label: "Thương hiệu", link: "/brand-list", icon: <Icon.Tag />,showSubRoute: false,submenu: false },
|
||||||
|
|||||||
@ -28,20 +28,20 @@ const ExpiredProduct = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const oldandlatestvalue = [
|
const oldandlatestvalue = [
|
||||||
{ value: 'date', label: 'Sort by Date' },
|
{ value: 'date', label: 'Sắp xếp theo ngày' },
|
||||||
{ value: 'newest', label: 'Newest' },
|
{ value: 'newest', label: 'Mới nhất' },
|
||||||
{ value: 'oldest', label: 'Oldest' },
|
{ value: 'oldest', label: 'Cũ nhất' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const brands = [
|
const brands = [
|
||||||
{ value: 'chooseType', label: 'Choose Type' },
|
{ value: 'chooseType', label: 'Chọn loại' },
|
||||||
{ value: 'lenovo3rdGen', label: 'Lenovo 3rd Generation' },
|
{ value: 'lenovo3rdGen', label: 'Lenovo thế hệ 3' },
|
||||||
{ value: 'nikeJordan', label: 'Nike Jordan' },
|
{ value: 'nikeJordan', label: 'Nike Jordan' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderTooltip = (props) => (
|
const renderTooltip = (props) => (
|
||||||
<Tooltip id="pdf-tooltip" {...props}>
|
<Tooltip id="pdf-tooltip" {...props}>
|
||||||
Pdf
|
PDF
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
const renderExcelTooltip = (props) => (
|
const renderExcelTooltip = (props) => (
|
||||||
@ -51,23 +51,23 @@ const ExpiredProduct = () => {
|
|||||||
);
|
);
|
||||||
const renderPrinterTooltip = (props) => (
|
const renderPrinterTooltip = (props) => (
|
||||||
<Tooltip id="printer-tooltip" {...props}>
|
<Tooltip id="printer-tooltip" {...props}>
|
||||||
Printer
|
In ấn
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
const renderRefreshTooltip = (props) => (
|
const renderRefreshTooltip = (props) => (
|
||||||
<Tooltip id="refresh-tooltip" {...props}>
|
<Tooltip id="refresh-tooltip" {...props}>
|
||||||
Refresh
|
Làm mới
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
const renderCollapseTooltip = (props) => (
|
const renderCollapseTooltip = (props) => (
|
||||||
<Tooltip id="refresh-tooltip" {...props}>
|
<Tooltip id="refresh-tooltip" {...props}>
|
||||||
Collapse
|
Thu gọn
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: "Product",
|
title: "Sản phẩm",
|
||||||
dataIndex: "product",
|
dataIndex: "product",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<span className="productimgname">
|
<span className="productimgname">
|
||||||
@ -81,23 +81,23 @@ const ExpiredProduct = () => {
|
|||||||
width: "5%"
|
width: "5%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "SKU",
|
title: "Mã IMEI",
|
||||||
dataIndex: "sku",
|
dataIndex: "sku",
|
||||||
sorter: (a, b) => a.sku.length - b.sku.length,
|
sorter: (a, b) => a.sku.length - b.sku.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Manufactured Date",
|
title: "Ngày sản xuất",
|
||||||
dataIndex: "manufactureddate",
|
dataIndex: "manufactureddate",
|
||||||
sorter: (a, b) => a.manufactureddate.length - b.manufactureddate.length,
|
sorter: (a, b) => a.manufactureddate.length - b.manufactureddate.length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Expired Date",
|
title: "Ngày hết hạn",
|
||||||
dataIndex: "expireddate",
|
dataIndex: "expireddate",
|
||||||
sorter: (a, b) => a.expireddate.length - b.expireddate.length,
|
sorter: (a, b) => a.expireddate.length - b.expireddate.length,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: 'Actions',
|
title: 'Thao tác',
|
||||||
dataIndex: 'actions',
|
dataIndex: 'actions',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
render: () => (
|
render: () => (
|
||||||
@ -118,19 +118,19 @@ const ExpiredProduct = () => {
|
|||||||
|
|
||||||
const showConfirmationAlert = () => {
|
const showConfirmationAlert = () => {
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
title: 'Are you sure?',
|
title: 'Bạn có chắc chắn?',
|
||||||
text: 'You won\'t be able to revert this!',
|
text: 'Bạn sẽ không thể hoàn tác hành động này!',
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: '#00ff00',
|
confirmButtonColor: '#00ff00',
|
||||||
confirmButtonText: 'Yes, delete it!',
|
confirmButtonText: 'Có, xóa nó!',
|
||||||
cancelButtonColor: '#ff0000',
|
cancelButtonColor: '#ff0000',
|
||||||
cancelButtonText: 'Cancel',
|
cancelButtonText: 'Hủy',
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
|
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
title: 'Deleted!',
|
title: 'Đã xóa!',
|
||||||
text: 'Your file has been deleted.',
|
text: 'Tệp của bạn đã được xóa.',
|
||||||
className: "btn btn-success",
|
className: "btn btn-success",
|
||||||
confirmButtonText: 'OK',
|
confirmButtonText: 'OK',
|
||||||
customClass: {
|
customClass: {
|
||||||
@ -150,8 +150,8 @@ const ExpiredProduct = () => {
|
|||||||
<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>Expired Products</h4>
|
<h4>Sản phẩm hết hạn</h4>
|
||||||
<h6>Manage your expired products</h6>
|
<h6>Quản lý các sản phẩm hết hạn của bạn</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="table-top-head">
|
<ul className="table-top-head">
|
||||||
@ -209,7 +209,7 @@ const ExpiredProduct = () => {
|
|||||||
<div className="search-input">
|
<div className="search-input">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
placeholder="Tìm kiếm"
|
||||||
className="form-control form-control-sm formsearch"
|
className="form-control form-control-sm formsearch"
|
||||||
/>
|
/>
|
||||||
<Link to className="btn btn-searchset">
|
<Link to className="btn btn-searchset">
|
||||||
@ -235,7 +235,7 @@ const ExpiredProduct = () => {
|
|||||||
<Select
|
<Select
|
||||||
className="select"
|
className="select"
|
||||||
options={oldandlatestvalue}
|
options={oldandlatestvalue}
|
||||||
placeholder="Newest"
|
placeholder="Mới nhất"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -251,7 +251,7 @@ const ExpiredProduct = () => {
|
|||||||
<div className="input-blocks">
|
<div className="input-blocks">
|
||||||
<Box className="info-img" />
|
<Box className="info-img" />
|
||||||
|
|
||||||
<Select options={brands} className="select" placeholder="Choose Type" />
|
<Select options={brands} className="select" placeholder="Chọn loại" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -264,7 +264,7 @@ const ExpiredProduct = () => {
|
|||||||
type="date"
|
type="date"
|
||||||
className="filterdatepicker"
|
className="filterdatepicker"
|
||||||
dateFormat="dd-MM-yyyy"
|
dateFormat="dd-MM-yyyy"
|
||||||
placeholder='Choose Date'
|
placeholder='Chọn ngày'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -274,7 +274,7 @@ const ExpiredProduct = () => {
|
|||||||
<Link className="btn btn-filters ms-auto">
|
<Link className="btn btn-filters ms-auto">
|
||||||
{" "}
|
{" "}
|
||||||
<i data-feather="search" className="feather-search" />{" "}
|
<i data-feather="search" className="feather-search" />{" "}
|
||||||
Search{" "}
|
Tìm kiếm{" "}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
784
src/feature-module/inventory/productlist2.jsx
Normal file
784
src/feature-module/inventory/productlist2.jsx
Normal file
@ -0,0 +1,784 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
ChevronUp,
|
||||||
|
Edit,
|
||||||
|
Eye,
|
||||||
|
Filter,
|
||||||
|
GitMerge,
|
||||||
|
PlusCircle,
|
||||||
|
RotateCcw,
|
||||||
|
Sliders,
|
||||||
|
StopCircle,
|
||||||
|
Trash2,
|
||||||
|
} from "feather-icons-react/build/IconComponents";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Select from "react-select";
|
||||||
|
import ImageWithBasePath from "../../core/img/imagewithbasebath";
|
||||||
|
import Brand from "../../core/modals/inventory/brand";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import { all_routes } from "../../Router/all_routes";
|
||||||
|
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||||
|
import Table from "../../core/pagination/datatable";
|
||||||
|
import {
|
||||||
|
fetchProducts,
|
||||||
|
fetchProduct,
|
||||||
|
deleteProduct,
|
||||||
|
searchProducts
|
||||||
|
} from "../../core/redux/actions/productActions";
|
||||||
|
import { setToogleHeader } from "../../core/redux/action";
|
||||||
|
import { Download } from "react-feather";
|
||||||
|
import CustomPagination from "../../components/CustomPagination";
|
||||||
|
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const route = all_routes;
|
||||||
|
|
||||||
|
// Add CSS to hide Ant Design pagination and checkboxes
|
||||||
|
function hideAntElements() {
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.type = "text/css";
|
||||||
|
styleSheet.innerText = `
|
||||||
|
.ant-pagination {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide all Ant Design checkboxes */
|
||||||
|
.ant-checkbox,
|
||||||
|
.ant-checkbox-input,
|
||||||
|
.ant-checkbox-wrapper,
|
||||||
|
.ant-table-selection-column,
|
||||||
|
.ant-table-selection,
|
||||||
|
.ant-table-thead > tr > th.ant-table-selection-column,
|
||||||
|
.ant-table-tbody > tr > td.ant-table-selection-column {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductList2 = () => {
|
||||||
|
// Use new Redux structure for API data, fallback to legacy for existing functionality
|
||||||
|
const {
|
||||||
|
products: apiProducts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
totalProducts,
|
||||||
|
pageSize: reduxPageSize,
|
||||||
|
currentPage: reduxCurrentPage
|
||||||
|
} = useSelector((state) => state.products);
|
||||||
|
|
||||||
|
// Fallback to legacy data if API data is not available
|
||||||
|
const legacyProducts = useSelector((state) => state.legacy?.product_list || []);
|
||||||
|
|
||||||
|
// Sample import data for demonstration
|
||||||
|
const sampleImportData = [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
product: "MacBook Pro 13 inch",
|
||||||
|
sku: "MBP13-001",
|
||||||
|
importPrice: 25000000,
|
||||||
|
importQuantity: 5,
|
||||||
|
productImage: "assets/img/products/macbook.jpg",
|
||||||
|
supplier: "Apple Vietnam",
|
||||||
|
createdDate: "2024-01-15",
|
||||||
|
importDate: "2024-01-15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
product: "iPhone 15 Pro",
|
||||||
|
sku: "IP15P-001",
|
||||||
|
importPrice: 28000000,
|
||||||
|
importQuantity: 10,
|
||||||
|
productImage: "assets/img/products/iphone.jpg",
|
||||||
|
supplier: "Apple Vietnam",
|
||||||
|
createdDate: "2024-01-14",
|
||||||
|
importDate: "2024-01-14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
product: "Samsung Galaxy S24",
|
||||||
|
sku: "SGS24-001",
|
||||||
|
importPrice: 22000000,
|
||||||
|
importQuantity: 8,
|
||||||
|
productImage: "assets/img/products/samsung.jpg",
|
||||||
|
supplier: "Samsung Vietnam",
|
||||||
|
createdDate: "2024-01-13",
|
||||||
|
importDate: "2024-01-13"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = apiProducts.length > 0 ? apiProducts : (legacyProducts.length > 0 ? legacyProducts : sampleImportData);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const data = useSelector((state) => state.legacy?.toggle_header || false);
|
||||||
|
|
||||||
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// State for pagination - sync with Redux
|
||||||
|
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
|
||||||
|
const [pageSize, setPageSize] = useState(reduxPageSize || 20);
|
||||||
|
|
||||||
|
// State for filter values
|
||||||
|
const [filterValues, setFilterValues] = useState({
|
||||||
|
product: '',
|
||||||
|
supplier: '',
|
||||||
|
importDate: '',
|
||||||
|
priceRange: '',
|
||||||
|
quantityRange: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total records and pages
|
||||||
|
const totalRecords = totalProducts || dataSource.length;
|
||||||
|
const actualTotalPages = Math.ceil(totalRecords / pageSize);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
hideAntElements();
|
||||||
|
|
||||||
|
// Fetch products on component mount
|
||||||
|
if (apiProducts.length === 0) {
|
||||||
|
dispatch(fetchProducts({ page: currentPage, pageSize }));
|
||||||
|
}
|
||||||
|
}, [dispatch, apiProducts.length]);
|
||||||
|
|
||||||
|
// Sync local state with Redux state
|
||||||
|
useEffect(() => {
|
||||||
|
if (reduxCurrentPage && reduxCurrentPage !== currentPage) {
|
||||||
|
setCurrentPage(reduxCurrentPage);
|
||||||
|
}
|
||||||
|
if (reduxPageSize && reduxPageSize !== pageSize) {
|
||||||
|
setPageSize(reduxPageSize);
|
||||||
|
}
|
||||||
|
}, [reduxCurrentPage, reduxPageSize, currentPage, pageSize]);
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchTerm(value);
|
||||||
|
|
||||||
|
if (value.trim()) {
|
||||||
|
dispatch(searchProducts(value));
|
||||||
|
} else {
|
||||||
|
dispatch(fetchProducts({ page: 1, pageSize }));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
dispatch(fetchProducts({ page, pageSize, search: searchTerm }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageSizeChange = (newPageSize) => {
|
||||||
|
setPageSize(newPageSize);
|
||||||
|
setCurrentPage(1);
|
||||||
|
dispatch(fetchProducts({ page: 1, pageSize: newPageSize, search: searchTerm }));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleDelete = async (productId) => {
|
||||||
|
try {
|
||||||
|
const result = await MySwal.fire({
|
||||||
|
title: "Are you sure?",
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#dc3545",
|
||||||
|
cancelButtonColor: "#6c757d",
|
||||||
|
confirmButtonText: "Yes, delete it!",
|
||||||
|
cancelButtonText: "Cancel",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
await dispatch(deleteProduct(productId));
|
||||||
|
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Deleted!",
|
||||||
|
text: "Product has been deleted successfully.",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#28a745",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh the product list
|
||||||
|
dispatch(fetchProducts({ page: currentPage, pageSize, search: searchTerm }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Error!",
|
||||||
|
text: "Failed to delete product. Please try again.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonColor: "#dc3545",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldandlatestvalue = [
|
||||||
|
{ value: "date", label: "Sắp xếp theo ngày" },
|
||||||
|
{ value: "newest", label: "Mới nhất" },
|
||||||
|
{ value: "oldest", label: "Cũ nhất" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const supplierOptions = [
|
||||||
|
{ value: "", label: "Chọn nhà cung cấp" },
|
||||||
|
{ value: "supplier1", label: "Nhà cung cấp A" },
|
||||||
|
{ value: "supplier2", label: "Nhà cung cấp B" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const importDateOptions = [
|
||||||
|
{ value: "", label: "Chọn thời gian nhập" },
|
||||||
|
{ value: "today", label: "Hôm nay" },
|
||||||
|
{ value: "week", label: "Tuần này" },
|
||||||
|
{ value: "month", label: "Tháng này" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const priceRangeOptions = [
|
||||||
|
{ value: "", label: "Chọn khoảng giá" },
|
||||||
|
{ value: "0-100000", label: "0 - 100,000 ₫" },
|
||||||
|
{ value: "100000-500000", label: "100,000 - 500,000 ₫" },
|
||||||
|
{ value: "500000-1000000", label: "500,000 - 1,000,000 ₫" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const quantityRangeOptions = [
|
||||||
|
{ value: "", label: "Chọn số lượng" },
|
||||||
|
{ value: "1-10", label: "1 - 10" },
|
||||||
|
{ value: "11-50", label: "11 - 50" },
|
||||||
|
{ value: "51-100", label: "51 - 100" },
|
||||||
|
{ value: "100+", label: "Trên 100" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleMenuToggle = () => {
|
||||||
|
dispatch(setToogleHeader(!data));
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "Sản phẩm",
|
||||||
|
dataIndex: "product",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span>{text || record.name || record.productName}</span>
|
||||||
|
),
|
||||||
|
sorter: (a, b) => a.product.length - b.product.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Mã",
|
||||||
|
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: "Giá nhập",
|
||||||
|
dataIndex: "importPrice",
|
||||||
|
render: (_, record) => {
|
||||||
|
const price = record.price || record.price || record.price || 0;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="price-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
color: '#856404',
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
minWidth: '80px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{price.toLocaleString('vi-VN')} ₫
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const priceA = a.importPrice || a.purchasePrice || a.costPrice || 0;
|
||||||
|
const priceB = b.importPrice || b.purchasePrice || b.costPrice || 0;
|
||||||
|
return priceA - priceB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Số lượng nhập",
|
||||||
|
dataIndex: "importQuantity",
|
||||||
|
render: (_, record) => {
|
||||||
|
const quantity = record.qty || record.quantity || record.inboundQuantity || 0;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="quantity-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: quantity > 0 ? '#e8f5e8' : '#f8f9fa',
|
||||||
|
color: quantity > 0 ? '#28a745' : '#6c757d',
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: `1px solid ${quantity > 0 ? '#c3e6cb' : '#dee2e6'}`,
|
||||||
|
minWidth: '80px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{quantity.toLocaleString('vi-VN')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const qtyA = a.importQuantity || a.receivedQuantity || a.inboundQuantity || 0;
|
||||||
|
const qtyB = b.importQuantity || b.receivedQuantity || b.inboundQuantity || 0;
|
||||||
|
return qtyA - qtyB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Ngày nhập",
|
||||||
|
dataIndex: "importDate",
|
||||||
|
render: (_, record) => {
|
||||||
|
const importDate = record.createdDate || record.importDate || record.receivedDate || '';
|
||||||
|
if (!importDate) return <span>-</span>;
|
||||||
|
|
||||||
|
// Format date to Vietnamese format
|
||||||
|
const date = new Date(importDate);
|
||||||
|
const formattedDate = date.toLocaleDateString('vi-VN', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate days ago
|
||||||
|
const today = new Date();
|
||||||
|
const diffTime = Math.abs(today - date);
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
let badgeColor = '#d1ecf1';
|
||||||
|
let textColor = '#0c5460';
|
||||||
|
let borderColor = '#bee5eb';
|
||||||
|
|
||||||
|
if (diffDays <= 1) {
|
||||||
|
badgeColor = '#d4edda';
|
||||||
|
textColor = '#155724';
|
||||||
|
borderColor = '#c3e6cb';
|
||||||
|
} else if (diffDays <= 7) {
|
||||||
|
badgeColor = '#fff3cd';
|
||||||
|
textColor = '#856404';
|
||||||
|
borderColor = '#ffeaa7';
|
||||||
|
} else if (diffDays > 30) {
|
||||||
|
badgeColor = '#f8d7da';
|
||||||
|
textColor = '#721c24';
|
||||||
|
borderColor = '#f5c6cb';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="date-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: badgeColor,
|
||||||
|
color: textColor,
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: `1px solid ${borderColor}`,
|
||||||
|
minWidth: '90px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
title={`${diffDays} ngày trước`}
|
||||||
|
>
|
||||||
|
{formattedDate}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const dateA = new Date(a.createdDate || a.importDate || a.receivedDate || 0);
|
||||||
|
const dateB = new Date(b.createdDate || b.importDate || b.receivedDate || 0);
|
||||||
|
return dateA - dateB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Thao tác",
|
||||||
|
dataIndex: "action",
|
||||||
|
render: (_, record) => (
|
||||||
|
<td className="action-table-data">
|
||||||
|
<div className="edit-delete-action">
|
||||||
|
<Link className="me-2 p-2" to={route.productdetails}>
|
||||||
|
<Eye className="feather-view" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="me-2 p-2"
|
||||||
|
to={`${route.editproduct}/${record.id || record.key}`}
|
||||||
|
onClick={() => {
|
||||||
|
// Pre-fetch product details for editing
|
||||||
|
if (record.id || record.key) {
|
||||||
|
dispatch(fetchProduct(record.id || record.key));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="feather-edit" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="confirm-text p-2"
|
||||||
|
to="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDelete(record.id || record.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="feather-delete" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-wrapper">
|
||||||
|
<div className="content">
|
||||||
|
<div className="page-header">
|
||||||
|
<div className="add-item d-flex">
|
||||||
|
<div className="page-title">
|
||||||
|
<h4>Nhập kho</h4>
|
||||||
|
<h6>Quản lý thông tin nhập kho sản phẩm</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="table-top-head">
|
||||||
|
<li>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={<Tooltip id="tooltip-top">Pdf</Tooltip>}
|
||||||
|
>
|
||||||
|
<Link>
|
||||||
|
<ImageWithBasePath
|
||||||
|
src="assets/img/icons/pdf.svg"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={<Tooltip id="tooltip-top">Excel</Tooltip>}
|
||||||
|
>
|
||||||
|
<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={<Tooltip id="tooltip-top">Print</Tooltip>}
|
||||||
|
>
|
||||||
|
<Link data-bs-toggle="tooltip" data-bs-placement="top">
|
||||||
|
<ImageWithBasePath
|
||||||
|
src="assets/img/icons/printer.svg"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={<Tooltip id="tooltip-top">Refresh</Tooltip>}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
onClick={() => dispatch(fetchProducts({ page: currentPage, pageSize }))}
|
||||||
|
>
|
||||||
|
<RotateCcw />
|
||||||
|
</Link>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={<Tooltip id="tooltip-top">Collapse</Tooltip>}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
id="collapse-header"
|
||||||
|
className={data ? "active" : ""}
|
||||||
|
onClick={handleMenuToggle}
|
||||||
|
>
|
||||||
|
<ChevronUp />
|
||||||
|
</Link>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="page-btn">
|
||||||
|
<Link to="#" className="btn btn-added">
|
||||||
|
<PlusCircle className="me-2 iconsize" />
|
||||||
|
Tạo phiếu nhập
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="page-btn import">
|
||||||
|
<Link
|
||||||
|
to="#"
|
||||||
|
className="btn btn-added color"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#view-notes"
|
||||||
|
>
|
||||||
|
<Download className="me-2" />
|
||||||
|
Import Excel
|
||||||
|
</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="Tìm kiếm sản phẩm nhập kho..."
|
||||||
|
className="form-control form-control-sm formsearch"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
<Link to className="btn btn-searchset">
|
||||||
|
<i data-feather="search" className="feather-search" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="search-path">
|
||||||
|
<Link
|
||||||
|
className={`btn btn-filter ${isFilterVisible ? "setclose" : ""}`}
|
||||||
|
id="filter_search"
|
||||||
|
onClick={() => setIsFilterVisible(!isFilterVisible)}
|
||||||
|
>
|
||||||
|
<Filter className="filter-icon" />
|
||||||
|
<span>
|
||||||
|
<ImageWithBasePath
|
||||||
|
src="assets/img/icons/closes.svg"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="form-sort">
|
||||||
|
<Sliders className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
options={oldandlatestvalue}
|
||||||
|
placeholder="Newest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /Filter */}
|
||||||
|
<div
|
||||||
|
className={`card${isFilterVisible ? " " : " d-none"}`}
|
||||||
|
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">
|
||||||
|
<GitMerge className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
options={[
|
||||||
|
{ value: "", label: "Chọn sản phẩm" },
|
||||||
|
{ value: "macbook-pro", label: "Macbook Pro" },
|
||||||
|
{ value: "orange", label: "Cam" },
|
||||||
|
]}
|
||||||
|
placeholder="Chọn sản phẩm"
|
||||||
|
value={[
|
||||||
|
{ value: "", label: "Chọn sản phẩm" },
|
||||||
|
{ value: "macbook-pro", label: "Macbook Pro" },
|
||||||
|
{ value: "orange", label: "Cam" },
|
||||||
|
].find(option => option.value === filterValues.product)}
|
||||||
|
onChange={(selectedOption) =>
|
||||||
|
setFilterValues(prev => ({ ...prev, product: selectedOption?.value || '' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-2 col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<StopCircle className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
options={supplierOptions}
|
||||||
|
placeholder="Chọn nhà cung cấp"
|
||||||
|
value={supplierOptions.find(option => option.value === filterValues.supplier)}
|
||||||
|
onChange={(selectedOption) =>
|
||||||
|
setFilterValues(prev => ({ ...prev, supplier: selectedOption?.value || '' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-2 col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<StopCircle className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
options={importDateOptions}
|
||||||
|
placeholder="Chọn thời gian nhập"
|
||||||
|
value={importDateOptions.find(option => option.value === filterValues.importDate)}
|
||||||
|
onChange={(selectedOption) =>
|
||||||
|
setFilterValues(prev => ({ ...prev, importDate: selectedOption?.value || '' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-2 col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<Box className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
options={priceRangeOptions}
|
||||||
|
placeholder="Chọn khoảng giá"
|
||||||
|
value={priceRangeOptions.find(option => option.value === filterValues.priceRange)}
|
||||||
|
onChange={(selectedOption) =>
|
||||||
|
setFilterValues(prev => ({ ...prev, priceRange: selectedOption?.value || '' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-2 col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<StopCircle className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
classNamePrefix="react-select"
|
||||||
|
options={quantityRangeOptions}
|
||||||
|
placeholder="Chọn số lượng"
|
||||||
|
value={quantityRangeOptions.find(option => option.value === filterValues.quantityRange)}
|
||||||
|
onChange={(selectedOption) =>
|
||||||
|
setFilterValues(prev => ({ ...prev, quantityRange: selectedOption?.value || '' }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-1 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"
|
||||||
|
style={{ marginRight: '8px' }}
|
||||||
|
/>
|
||||||
|
Tìm kiếm
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /Filter */}
|
||||||
|
<div className="table-responsive">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center p-4">
|
||||||
|
<div className="spinner-border text-primary" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2">Đang tải dữ liệu nhập kho...</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(fetchProducts())}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={dataSource}
|
||||||
|
pagination={false} // Disable Ant Design pagination
|
||||||
|
rowSelection={null} // Disable row selection checkboxes
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Reusable Custom Pagination Component */}
|
||||||
|
<CustomPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
pageSize={pageSize}
|
||||||
|
totalCount={totalRecords}
|
||||||
|
totalPages={actualTotalPages}
|
||||||
|
loading={loading}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
pageSizeOptions={[10, 20, 50, 100]}
|
||||||
|
showInfo={true}
|
||||||
|
showPageSizeSelector={true}
|
||||||
|
compact={false}
|
||||||
|
className="product-list-pagination"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /product list */}
|
||||||
|
<Brand />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductList2;
|
||||||
759
src/feature-module/inventory/productlist3.jsx
Normal file
759
src/feature-module/inventory/productlist3.jsx
Normal file
@ -0,0 +1,759 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Edit,
|
||||||
|
Eye,
|
||||||
|
Filter,
|
||||||
|
GitMerge,
|
||||||
|
Sliders,
|
||||||
|
StopCircle,
|
||||||
|
Trash2,
|
||||||
|
} from "feather-icons-react/build/IconComponents";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Select from "react-select";
|
||||||
|
import ImageWithBasePath from "../../core/img/imagewithbasebath";
|
||||||
|
import Brand from "../../core/modals/inventory/brand";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import { all_routes } from "../../Router/all_routes";
|
||||||
|
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||||
|
import Table from "../../core/pagination/datatable";
|
||||||
|
import {
|
||||||
|
fetchProducts,
|
||||||
|
fetchProduct,
|
||||||
|
deleteProduct,
|
||||||
|
searchProducts
|
||||||
|
} from "../../core/redux/actions/productActions";
|
||||||
|
|
||||||
|
import CustomPagination from "../../components/CustomPagination";
|
||||||
|
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const route = all_routes;
|
||||||
|
|
||||||
|
// Add CSS to hide Ant Design pagination and checkboxes
|
||||||
|
function hideAntElements() {
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.type = "text/css";
|
||||||
|
styleSheet.innerText = `
|
||||||
|
.ant-pagination {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide all Ant Design checkboxes */
|
||||||
|
.ant-checkbox,
|
||||||
|
.ant-checkbox-input,
|
||||||
|
.ant-checkbox-wrapper,
|
||||||
|
.ant-table-selection-column,
|
||||||
|
.ant-table-selection,
|
||||||
|
.ant-table-thead > tr > th.ant-table-selection-column,
|
||||||
|
.ant-table-tbody > tr > td.ant-table-selection-column {
|
||||||
|
display: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductList3 = () => {
|
||||||
|
// Use new Redux structure for API data, fallback to legacy for existing functionality
|
||||||
|
const {
|
||||||
|
products: apiProducts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
totalProducts,
|
||||||
|
pageSize: reduxPageSize,
|
||||||
|
currentPage: reduxCurrentPage
|
||||||
|
} = useSelector((state) => state.products);
|
||||||
|
|
||||||
|
// Fallback to legacy data if API data is not available
|
||||||
|
const legacyProducts = useSelector((state) => state.legacy?.product_list || []);
|
||||||
|
|
||||||
|
// Sample inventory data for demonstration
|
||||||
|
const sampleInventoryData = [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
product: "MacBook Pro 13 inch",
|
||||||
|
sku: "MBP13-001",
|
||||||
|
totalImported: 50,
|
||||||
|
totalExported: 35,
|
||||||
|
currentStock: 15,
|
||||||
|
minStock: 5,
|
||||||
|
maxStock: 50,
|
||||||
|
unitPrice: 25000000,
|
||||||
|
totalValue: 375000000,
|
||||||
|
productImage: "assets/img/products/macbook.jpg",
|
||||||
|
createdDate: "2024-01-15",
|
||||||
|
lastUpdated: "2024-01-20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 2,
|
||||||
|
product: "iPhone 15 Pro",
|
||||||
|
sku: "IP15P-001",
|
||||||
|
totalImported: 80,
|
||||||
|
totalExported: 55,
|
||||||
|
currentStock: 25,
|
||||||
|
minStock: 10,
|
||||||
|
maxStock: 100,
|
||||||
|
unitPrice: 28000000,
|
||||||
|
totalValue: 700000000,
|
||||||
|
productImage: "assets/img/products/iphone.jpg",
|
||||||
|
createdDate: "2024-01-14",
|
||||||
|
lastUpdated: "2024-01-19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 3,
|
||||||
|
product: "Samsung Galaxy S24",
|
||||||
|
sku: "SGS24-001",
|
||||||
|
totalImported: 30,
|
||||||
|
totalExported: 22,
|
||||||
|
currentStock: 8,
|
||||||
|
minStock: 5,
|
||||||
|
maxStock: 30,
|
||||||
|
unitPrice: 22000000,
|
||||||
|
totalValue: 176000000,
|
||||||
|
productImage: "assets/img/products/samsung.jpg",
|
||||||
|
createdDate: "2024-01-13",
|
||||||
|
lastUpdated: "2024-01-18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 4,
|
||||||
|
product: "Dell XPS 13",
|
||||||
|
sku: "DXP13-001",
|
||||||
|
totalImported: 40,
|
||||||
|
totalExported: 28,
|
||||||
|
currentStock: 12,
|
||||||
|
minStock: 8,
|
||||||
|
maxStock: 40,
|
||||||
|
unitPrice: 23000000,
|
||||||
|
totalValue: 276000000,
|
||||||
|
productImage: "assets/img/products/dell.jpg",
|
||||||
|
createdDate: "2024-01-12",
|
||||||
|
lastUpdated: "2024-01-17"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 5,
|
||||||
|
product: "iPad Pro 11 inch",
|
||||||
|
sku: "IPD11-001",
|
||||||
|
totalImported: 60,
|
||||||
|
totalExported: 45,
|
||||||
|
currentStock: 15,
|
||||||
|
minStock: 10,
|
||||||
|
maxStock: 60,
|
||||||
|
unitPrice: 20000000,
|
||||||
|
totalValue: 300000000,
|
||||||
|
productImage: "assets/img/products/ipad.jpg",
|
||||||
|
createdDate: "2024-01-11",
|
||||||
|
lastUpdated: "2024-01-16"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = apiProducts.length > 0 ? apiProducts : (legacyProducts.length > 0 ? legacyProducts : sampleInventoryData);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// State for pagination - sync with Redux
|
||||||
|
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
|
||||||
|
const [pageSize, setPageSize] = useState(reduxPageSize || 20);
|
||||||
|
|
||||||
|
// State for filter values
|
||||||
|
const [filterValues, setFilterValues] = useState({
|
||||||
|
product: '',
|
||||||
|
stockLevel: '',
|
||||||
|
quantityRange: '',
|
||||||
|
stockStatus: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total records and pages
|
||||||
|
const totalRecords = totalProducts || dataSource.length;
|
||||||
|
const actualTotalPages = Math.ceil(totalRecords / pageSize);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
hideAntElements();
|
||||||
|
|
||||||
|
// Fetch products on component mount
|
||||||
|
if (apiProducts.length === 0) {
|
||||||
|
dispatch(fetchProducts({ page: currentPage, pageSize }));
|
||||||
|
}
|
||||||
|
}, [dispatch, apiProducts.length]);
|
||||||
|
|
||||||
|
// Sync local state with Redux state
|
||||||
|
useEffect(() => {
|
||||||
|
if (reduxCurrentPage && reduxCurrentPage !== currentPage) {
|
||||||
|
setCurrentPage(reduxCurrentPage);
|
||||||
|
}
|
||||||
|
if (reduxPageSize && reduxPageSize !== pageSize) {
|
||||||
|
setPageSize(reduxPageSize);
|
||||||
|
}
|
||||||
|
}, [reduxCurrentPage, reduxPageSize, currentPage, pageSize]);
|
||||||
|
|
||||||
|
const handleSearch = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSearchTerm(value);
|
||||||
|
|
||||||
|
if (value.trim()) {
|
||||||
|
dispatch(searchProducts(value));
|
||||||
|
} else {
|
||||||
|
dispatch(fetchProducts({ page: 1, pageSize }));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
dispatch(fetchProducts({ page, pageSize, search: searchTerm }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageSizeChange = (newPageSize) => {
|
||||||
|
setPageSize(newPageSize);
|
||||||
|
setCurrentPage(1);
|
||||||
|
dispatch(fetchProducts({ page: 1, pageSize: newPageSize, search: searchTerm }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (productId) => {
|
||||||
|
try {
|
||||||
|
const result = await MySwal.fire({
|
||||||
|
title: "Are you sure?",
|
||||||
|
text: "You won't be able to revert this!",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#dc3545",
|
||||||
|
cancelButtonColor: "#6c757d",
|
||||||
|
confirmButtonText: "Yes, delete it!",
|
||||||
|
cancelButtonText: "Cancel",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
await dispatch(deleteProduct(productId));
|
||||||
|
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Deleted!",
|
||||||
|
text: "Product has been deleted successfully.",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#28a745",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh the product list
|
||||||
|
dispatch(fetchProducts({ page: currentPage, pageSize, search: searchTerm }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Error!",
|
||||||
|
text: "Failed to delete product. Please try again.",
|
||||||
|
icon: "error",
|
||||||
|
confirmButtonColor: "#dc3545",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldandlatestvalue = [
|
||||||
|
{ value: "date", label: "Sắp xếp theo ngày" },
|
||||||
|
{ value: "newest", label: "Mới nhất" },
|
||||||
|
{ value: "oldest", label: "Cũ nhất" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stockLevelOptions = [
|
||||||
|
{ value: "", label: "Chọn mức tồn kho" },
|
||||||
|
{ value: "low", label: "Tồn kho thấp" },
|
||||||
|
{ value: "normal", label: "Tồn kho bình thường" },
|
||||||
|
{ value: "high", label: "Tồn kho cao" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const stockStatusOptions = [
|
||||||
|
{ value: "", label: "Chọn trạng thái" },
|
||||||
|
{ value: "in-stock", label: "Còn hàng" },
|
||||||
|
{ value: "low-stock", label: "Sắp hết hàng" },
|
||||||
|
{ value: "out-of-stock", label: "Hết hàng" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const quantityRangeOptions = [
|
||||||
|
{ value: "", label: "Chọn khoảng số lượng" },
|
||||||
|
{ value: "0-10", label: "0 - 10" },
|
||||||
|
{ value: "11-50", label: "11 - 50" },
|
||||||
|
{ value: "51-100", label: "51 - 100" },
|
||||||
|
{ value: "100+", label: "Trên 100" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Handle filter value changes
|
||||||
|
const handleFilterChange = (filterType, value) => {
|
||||||
|
setFilterValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[filterType]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "Sản phẩm",
|
||||||
|
dataIndex: "product",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span>{text || record.name || record.productName}</span>
|
||||||
|
),
|
||||||
|
sorter: (a, b) => a.product.length - b.product.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Mã",
|
||||||
|
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: "Tổng nhập kho",
|
||||||
|
dataIndex: "totalImported",
|
||||||
|
render: (_, record) => {
|
||||||
|
const totalImported = record.totalImported || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="import-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: '#e8f5e8',
|
||||||
|
color: '#28a745',
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: '1px solid #c3e6cb',
|
||||||
|
minWidth: '80px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totalImported.toLocaleString('vi-VN')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const importedA = a.totalImported || 0;
|
||||||
|
const importedB = b.totalImported || 0;
|
||||||
|
return importedA - importedB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tổng xuất kho",
|
||||||
|
dataIndex: "totalExported",
|
||||||
|
render: (_, record) => {
|
||||||
|
const totalExported = record.totalExported || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="export-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
color: '#856404',
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
minWidth: '80px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totalExported.toLocaleString('vi-VN')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const exportedA = a.totalExported || 0;
|
||||||
|
const exportedB = b.totalExported || 0;
|
||||||
|
return exportedA - exportedB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tồn kho hiện tại",
|
||||||
|
dataIndex: "currentStock",
|
||||||
|
render: (_, record) => {
|
||||||
|
const stock = record.currentStock || record.qty || record.quantity || 0;
|
||||||
|
const minStock = record.minStock || 5;
|
||||||
|
|
||||||
|
let badgeColor = '#e3f2fd';
|
||||||
|
let textColor = '#1565c0';
|
||||||
|
let borderColor = '#bbdefb';
|
||||||
|
|
||||||
|
if (stock <= 0) {
|
||||||
|
badgeColor = '#f8d7da';
|
||||||
|
textColor = '#721c24';
|
||||||
|
borderColor = '#f5c6cb';
|
||||||
|
} else if (stock <= minStock) {
|
||||||
|
badgeColor = '#fff3cd';
|
||||||
|
textColor = '#856404';
|
||||||
|
borderColor = '#ffeaa7';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="stock-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: badgeColor,
|
||||||
|
color: textColor,
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: `1px solid ${borderColor}`,
|
||||||
|
minWidth: '80px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stock.toLocaleString('vi-VN')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const stockA = a.currentStock || a.qty || a.quantity || 0;
|
||||||
|
const stockB = b.currentStock || b.qty || b.quantity || 0;
|
||||||
|
return stockA - stockB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Giá trị tồn kho",
|
||||||
|
dataIndex: "totalValue",
|
||||||
|
render: (_, record) => {
|
||||||
|
const stock = record.currentStock || record.qty || record.quantity || 0;
|
||||||
|
const price = record.unitPrice || record.price || record.sellPrice || 0;
|
||||||
|
const totalValue = stock * price;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="value-badge"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '6px 12px',
|
||||||
|
backgroundColor: '#f3e5f5',
|
||||||
|
color: '#7b1fa2',
|
||||||
|
borderRadius: '20px',
|
||||||
|
fontSize: '13px',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: '1px solid #e1bee7',
|
||||||
|
minWidth: '120px',
|
||||||
|
textAlign: 'center',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(-1px)';
|
||||||
|
e.target.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.transform = 'translateY(0)';
|
||||||
|
e.target.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totalValue.toLocaleString('vi-VN')} ₫
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorter: (a, b) => {
|
||||||
|
const valueA = (a.currentStock || 0) * (a.unitPrice || 0);
|
||||||
|
const valueB = (b.currentStock || 0) * (b.unitPrice || 0);
|
||||||
|
return valueA - valueB;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Thao tác",
|
||||||
|
dataIndex: "action",
|
||||||
|
render: (_, record) => (
|
||||||
|
<td className="action-table-data">
|
||||||
|
<div className="edit-delete-action">
|
||||||
|
<Link className="me-2 p-2" to={route.productdetails}>
|
||||||
|
<Eye className="feather-view" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="me-2 p-2"
|
||||||
|
to={`${route.editproduct}/${record.id || record.key}`}
|
||||||
|
onClick={() => {
|
||||||
|
// Pre-fetch product details for editing
|
||||||
|
if (record.id || record.key) {
|
||||||
|
dispatch(fetchProduct(record.id || record.key));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="feather-edit" />
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className="confirm-text p-2"
|
||||||
|
to="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDelete(record.id || record.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="feather-delete" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-wrapper">
|
||||||
|
<div className="content">
|
||||||
|
<div className="page-header">
|
||||||
|
<div className="add-item d-flex">
|
||||||
|
<div className="page-title">
|
||||||
|
<h4>Tồn kho</h4>
|
||||||
|
<h6>Quản lý thông tin tồn kho sản phẩm</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul className="table-top-head">
|
||||||
|
<li>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={<Tooltip id="tooltip-top">Pdf</Tooltip>}
|
||||||
|
>
|
||||||
|
<Link>
|
||||||
|
<ImageWithBasePath
|
||||||
|
src="assets/img/icons/pdf.svg"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<OverlayTrigger
|
||||||
|
placement="top"
|
||||||
|
overlay={<Tooltip id="tooltip-top">Excel</Tooltip>}
|
||||||
|
>
|
||||||
|
<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={<Tooltip id="tooltip-top">Print</Tooltip>}
|
||||||
|
>
|
||||||
|
<Link data-bs-toggle="tooltip" data-bs-placement="top">
|
||||||
|
<ImageWithBasePath
|
||||||
|
src="assets/img/icons/printer.svg"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/* /product list */}
|
||||||
|
<div className="card mb-0" id="filter_inputs">
|
||||||
|
<div className="card-body pb-0">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-12 col-sm-12">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<StopCircle className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
options={oldandlatestvalue}
|
||||||
|
placeholder="Sắp xếp theo ngày"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<Box className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
options={stockLevelOptions}
|
||||||
|
placeholder="Chọn mức tồn kho"
|
||||||
|
value={stockLevelOptions.find(option => option.value === filterValues.stockLevel)}
|
||||||
|
onChange={(selectedOption) => handleFilterChange('stockLevel', selectedOption?.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<Sliders className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
options={quantityRangeOptions}
|
||||||
|
placeholder="Chọn khoảng số lượng"
|
||||||
|
value={quantityRangeOptions.find(option => option.value === filterValues.quantityRange)}
|
||||||
|
onChange={(selectedOption) => handleFilterChange('quantityRange', selectedOption?.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<Filter className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
options={stockStatusOptions}
|
||||||
|
placeholder="Chọn trạng thái"
|
||||||
|
value={stockStatusOptions.find(option => option.value === filterValues.stockStatus)}
|
||||||
|
onChange={(selectedOption) => handleFilterChange('stockStatus', selectedOption?.value || '')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-1 col-sm-6 col-12">
|
||||||
|
<div className="input-blocks">
|
||||||
|
<Link className="btn btn-filters ms-auto">
|
||||||
|
<i data-feather="search" className="feather-search" />
|
||||||
|
Tìm kiếm
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /Filter */}
|
||||||
|
<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="Tìm kiếm sản phẩm tồn kho..."
|
||||||
|
className="form-control form-control-sm formsearch"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
<Link 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"
|
||||||
|
onClick={() => setIsFilterVisible(!isFilterVisible)}
|
||||||
|
>
|
||||||
|
<Filter className="filter-icon" />
|
||||||
|
<span>
|
||||||
|
<ImageWithBasePath
|
||||||
|
src="assets/img/icons/closes.svg"
|
||||||
|
alt="img"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="form-sort">
|
||||||
|
<Sliders className="info-img" />
|
||||||
|
<Select
|
||||||
|
className="img-select"
|
||||||
|
options={oldandlatestvalue}
|
||||||
|
placeholder="Sắp xếp theo ngày"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /Filter */}
|
||||||
|
<div className="table-responsive">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center p-4">
|
||||||
|
<div className="spinner-border text-primary" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2">Đang tải dữ liệu tồn kho...</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center p-4">
|
||||||
|
<div className="alert alert-danger">
|
||||||
|
<p>Lỗi: {error}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => dispatch(fetchProducts({ page: currentPage, pageSize }))}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={dataSource}
|
||||||
|
pagination={false} // Disable Ant Design pagination
|
||||||
|
rowSelection={null} // Disable row selection checkboxes
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Custom Pagination */}
|
||||||
|
<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="inventory-pagination"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* /product list */}
|
||||||
|
<Brand />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductList3;
|
||||||
Loading…
x
Reference in New Issue
Block a user