From 1171d7c4524c4c9ad5ce9bcb20b919c1863f261d Mon Sep 17 00:00:00 2001 From: "tuan.cna" Date: Tue, 17 Jun 2025 23:41:29 +0700 Subject: [PATCH] feat: Add inventory management pages with enhanced features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/Router/all_routes.jsx | 2 + src/Router/router.link.jsx | 16 + src/core/json/siderbar_data.jsx | 6 +- .../inventory/expiredproduct.jsx | 56 +- src/feature-module/inventory/productlist2.jsx | 784 ++++++++++++++++++ src/feature-module/inventory/productlist3.jsx | 759 +++++++++++++++++ 6 files changed, 1593 insertions(+), 30 deletions(-) create mode 100644 src/feature-module/inventory/productlist2.jsx create mode 100644 src/feature-module/inventory/productlist3.jsx diff --git a/src/Router/all_routes.jsx b/src/Router/all_routes.jsx index cf3160d..fb70916 100644 --- a/src/Router/all_routes.jsx +++ b/src/Router/all_routes.jsx @@ -1,6 +1,8 @@ export const all_routes = { dashboard: "/", productlist: "/product-list", + productlist2: "/product-list-2", + productlist3: "/product-list-3", addproduct: "/add-product", salesdashboard: "/sales-dashboard", brandlist: "/brand-list", diff --git a/src/Router/router.link.jsx b/src/Router/router.link.jsx index 3bb1758..b299828 100644 --- a/src/Router/router.link.jsx +++ b/src/Router/router.link.jsx @@ -199,6 +199,8 @@ import ProjectTracker from "../feature-module/projects/projecttracker"; import CreateProject from "../feature-module/projects/createproject"; import EnhancedLoaders from "../feature-module/uiinterface/enhanced-loaders"; 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"; export const publicRoutes = [ { @@ -215,6 +217,20 @@ export const publicRoutes = [ element: , route: Route, }, + { + id: 2.1, + path: routes.productlist2, + name: "products2", + element: , + route: Route, + }, + { + id: 2.2, + path: routes.productlist3, + name: "products3", + element: , + route: Route, + }, { id: 3, path: routes.addproduct, diff --git a/src/core/json/siderbar_data.jsx b/src/core/json/siderbar_data.jsx index ade97db..e59ed4d 100644 --- a/src/core/json/siderbar_data.jsx +++ b/src/core/json/siderbar_data.jsx @@ -62,9 +62,11 @@ export const SidebarData = [ submenuItems: [ { label: "Tiến độ dự án", link: "/project-tracker",icon: ,showSubRoute: false}, { label: "Sản phẩm", link: "/product-list", icon:,showSubRoute: false,submenu: false }, + { label: "Nhập kho", link: "/product-list-2", icon:,showSubRoute: false,submenu: false }, + { label: "Tồn kho", link: "/product-list-3", icon:,showSubRoute: false,submenu: false }, { label: "Create Product", link: "/add-product", icon: ,showSubRoute: false, submenu: false }, - { label: "Expired Products", link: "/expired-products", icon: ,showSubRoute: false,submenu: false }, - { label: "Low Stocks", link: "/low-stocks", icon: ,showSubRoute: false,submenu: false }, + { label: "Sản phẩm hết hạn", link: "/expired-products", icon: ,showSubRoute: false,submenu: false }, + { label: "Hàng tồn kho", link: "/low-stocks", icon: ,showSubRoute: false,submenu: false }, { label: "Danh mục", link: "/category-list", icon: ,showSubRoute: false,submenu: false }, { label: "Sub Category", link: "/sub-categories", icon: ,showSubRoute: false,submenu: false }, { label: "Thương hiệu", link: "/brand-list", icon: ,showSubRoute: false,submenu: false }, diff --git a/src/feature-module/inventory/expiredproduct.jsx b/src/feature-module/inventory/expiredproduct.jsx index 9adb2e0..d9cb0c6 100644 --- a/src/feature-module/inventory/expiredproduct.jsx +++ b/src/feature-module/inventory/expiredproduct.jsx @@ -28,20 +28,20 @@ const ExpiredProduct = () => { }; const oldandlatestvalue = [ - { value: 'date', label: 'Sort by Date' }, - { value: 'newest', label: 'Newest' }, - { value: 'oldest', label: 'Oldest' }, + { value: 'date', label: 'Sắp xếp theo ngày' }, + { value: 'newest', label: 'Mới nhất' }, + { value: 'oldest', label: 'Cũ nhất' }, ]; const brands = [ - { value: 'chooseType', label: 'Choose Type' }, - { value: 'lenovo3rdGen', label: 'Lenovo 3rd Generation' }, + { value: 'chooseType', label: 'Chọn loại' }, + { value: 'lenovo3rdGen', label: 'Lenovo thế hệ 3' }, { value: 'nikeJordan', label: 'Nike Jordan' }, ]; const renderTooltip = (props) => ( - Pdf + PDF ); const renderExcelTooltip = (props) => ( @@ -51,23 +51,23 @@ const ExpiredProduct = () => { ); const renderPrinterTooltip = (props) => ( - Printer + In ấn ); const renderRefreshTooltip = (props) => ( - Refresh + Làm mới ); const renderCollapseTooltip = (props) => ( - Collapse + Thu gọn ); const columns = [ { - title: "Product", + title: "Sản phẩm", dataIndex: "product", render: (text, record) => ( @@ -81,23 +81,23 @@ const ExpiredProduct = () => { width: "5%" }, { - title: "SKU", + title: "Mã IMEI", dataIndex: "sku", sorter: (a, b) => a.sku.length - b.sku.length, }, { - title: "Manufactured Date", + title: "Ngày sản xuất", dataIndex: "manufactureddate", sorter: (a, b) => a.manufactureddate.length - b.manufactureddate.length, }, { - title: "Expired Date", + title: "Ngày hết hạn", dataIndex: "expireddate", sorter: (a, b) => a.expireddate.length - b.expireddate.length, }, - + { - title: 'Actions', + title: 'Thao tác', dataIndex: 'actions', key: 'actions', render: () => ( @@ -118,19 +118,19 @@ const ExpiredProduct = () => { const showConfirmationAlert = () => { MySwal.fire({ - title: 'Are you sure?', - text: 'You won\'t be able to revert this!', + title: 'Bạn có chắc chắn?', + text: 'Bạn sẽ không thể hoàn tác hành động này!', showCancelButton: true, confirmButtonColor: '#00ff00', - confirmButtonText: 'Yes, delete it!', + confirmButtonText: 'Có, xóa nó!', cancelButtonColor: '#ff0000', - cancelButtonText: 'Cancel', + cancelButtonText: 'Hủy', }).then((result) => { if (result.isConfirmed) { MySwal.fire({ - title: 'Deleted!', - text: 'Your file has been deleted.', + title: 'Đã xóa!', + text: 'Tệp của bạn đã được xóa.', className: "btn btn-success", confirmButtonText: 'OK', customClass: { @@ -150,8 +150,8 @@ const ExpiredProduct = () => {
-

Expired Products

-
Manage your expired products
+

Sản phẩm hết hạn

+
Quản lý các sản phẩm hết hạn của bạn
    @@ -209,7 +209,7 @@ const ExpiredProduct = () => {
    @@ -235,7 +235,7 @@ const ExpiredProduct = () => { + + + + +
    +
+
+ setIsFilterVisible(!isFilterVisible)} + > + + + + + +
+
+ + option.value === filterValues.product)} + onChange={(selectedOption) => + setFilterValues(prev => ({ ...prev, product: selectedOption?.value || '' })) + } + /> +
+ +
+
+ + option.value === filterValues.importDate)} + onChange={(selectedOption) => + setFilterValues(prev => ({ ...prev, importDate: selectedOption?.value || '' })) + } + /> +
+
+
+
+ + option.value === filterValues.quantityRange)} + onChange={(selectedOption) => + setFilterValues(prev => ({ ...prev, quantityRange: selectedOption?.value || '' })) + } + /> +
+
+
+
+ + + Tìm kiếm + +
+
+ + + + {/* /Filter */} +
+ {loading ? ( +
+
+ Loading... +
+

Đang tải dữ liệu nhập kho...

+
+ ) : error ? ( +
+ Error: {error} + +
+ ) : ( + <> + + + {/* Reusable Custom Pagination Component */} + + + )} + + + + {/* /product list */} + + + + ); +}; + +export default ProductList2; diff --git a/src/feature-module/inventory/productlist3.jsx b/src/feature-module/inventory/productlist3.jsx new file mode 100644 index 0000000..cb68f19 --- /dev/null +++ b/src/feature-module/inventory/productlist3.jsx @@ -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) => ( + {text || record.name || record.productName} + ), + sorter: (a, b) => a.product.length - b.product.length, + }, + { + title: "Mã", + dataIndex: "sku", + render: (_, record) => { + const sku = record.sku || record.code || record.productCode || '-'; + return {sku}; + }, + 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 ( + { + 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')} + + ); + }, + 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 ( + { + 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')} + + ); + }, + 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 ( + { + 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')} + + ); + }, + 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 ( + { + 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')} ₫ + + ); + }, + 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) => ( + + ), + }, + ]; + + return ( +
+
+
+
+
+

Tồn kho

+
Quản lý thông tin tồn kho sản phẩm
+
+
+
    +
  • + Pdf} + > + + + + +
  • +
  • + Excel} + > + + + + +
  • +
  • + Print} + > + + + + +
  • +
+
+ {/* /product list */} +
+
+
+
+
+
+
+ + option.value === filterValues.stockLevel)} + onChange={(selectedOption) => handleFilterChange('stockLevel', selectedOption?.value || '')} + /> +
+
+
+
+ + option.value === filterValues.stockStatus)} + onChange={(selectedOption) => handleFilterChange('stockStatus', selectedOption?.value || '')} + /> +
+
+
+
+ + + Tìm kiếm + +
+
+
+
+
+
+
+ {/* /Filter */} +
+
+
+
+
+ + + + +
+
+
+ setIsFilterVisible(!isFilterVisible)} + > + + + + + +
+
+ +
+
+ + + + { + // Pre-fetch product details for editing + if (record.id || record.key) { + dispatch(fetchProduct(record.id || record.key)); + } + }} + > + + + { + e.preventDefault(); + handleDelete(record.id || record.key); + }} + > + + +
+
+ + {/* Custom Pagination */} + + + )} + + + + {/* /product list */} + + + + ); +}; + +export default ProductList3;