525 lines
17 KiB
JavaScript
525 lines
17 KiB
JavaScript
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 { setToogleHeader } from "../../core/redux/action";
|
|
import { Download } from "react-feather";
|
|
import {
|
|
fetchProducts,
|
|
fetchProduct,
|
|
deleteProduct,
|
|
clearProductError
|
|
} from "../../core/redux/actions/productActions";
|
|
|
|
const ProductList = () => {
|
|
// Use new Redux structure for API data, fallback to legacy for existing functionality
|
|
const {
|
|
products: apiProducts,
|
|
loading,
|
|
error
|
|
} = useSelector((state) => state.products);
|
|
|
|
// Fallback to legacy data if API data is not available
|
|
const legacyProducts = useSelector((state) => state.legacy?.product_list || []);
|
|
const dataSource = apiProducts.length > 0 ? apiProducts : legacyProducts;
|
|
|
|
const dispatch = useDispatch();
|
|
const data = useSelector((state) => state.legacy?.toggle_header || false);
|
|
|
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
const toggleFilterVisibility = () => {
|
|
setIsFilterVisible((prevVisibility) => !prevVisibility);
|
|
};
|
|
|
|
const route = all_routes;
|
|
|
|
// Fetch products on component mount
|
|
useEffect(() => {
|
|
const loadProducts = async () => {
|
|
try {
|
|
await dispatch(fetchProducts());
|
|
// Only fetch products - categories/brands may be included in response
|
|
// or can be extracted from products data
|
|
} catch (error) {
|
|
console.error('Failed to load products:', error);
|
|
}
|
|
};
|
|
|
|
loadProducts();
|
|
}, [dispatch]);
|
|
|
|
// Handle product deletion
|
|
const handleDeleteProduct = async (productId) => {
|
|
try {
|
|
await dispatch(deleteProduct(productId));
|
|
// Show success message
|
|
MySwal.fire({
|
|
title: "Deleted!",
|
|
text: "Product has been deleted successfully.",
|
|
icon: "success",
|
|
className: "btn btn-success",
|
|
customClass: {
|
|
confirmButton: "btn btn-success",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to delete product:', error);
|
|
MySwal.fire({
|
|
title: "Error!",
|
|
text: "Failed to delete product. Please try again.",
|
|
icon: "error",
|
|
className: "btn btn-danger",
|
|
customClass: {
|
|
confirmButton: "btn btn-danger",
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle search
|
|
const handleSearch = (e) => {
|
|
const value = e.target.value;
|
|
setSearchTerm(value);
|
|
// You can implement debounced search here
|
|
// For now, we'll just update the search term
|
|
};
|
|
|
|
// Clear error when component unmounts
|
|
useEffect(() => {
|
|
return () => {
|
|
dispatch(clearProductError());
|
|
};
|
|
}, [dispatch]);
|
|
const options = [
|
|
{ value: "sortByDate", label: "Sort by Date" },
|
|
{ value: "140923", label: "14 09 23" },
|
|
{ value: "110923", label: "11 09 23" },
|
|
];
|
|
const productlist = [
|
|
{ value: "choose", label: "Choose Product" },
|
|
{ value: "lenovo", label: "Lenovo 3rd Generation" },
|
|
{ value: "nike", label: "Nike Jordan" },
|
|
];
|
|
const categorylist = [
|
|
{ value: "choose", label: "Choose Category" },
|
|
{ value: "laptop", label: "Laptop" },
|
|
{ value: "shoe", label: "Shoe" },
|
|
];
|
|
const subcategorylist = [
|
|
{ value: "choose", label: "Choose Sub Category" },
|
|
{ value: "computers", label: "Computers" },
|
|
{ value: "fruits", label: "Fruits" },
|
|
];
|
|
const brandlist = [
|
|
{ value: "all", label: "All Brand" },
|
|
{ value: "lenovo", label: "Lenovo" },
|
|
{ value: "nike", label: "Nike" },
|
|
];
|
|
const price = [
|
|
{ value: "price", label: "Price" },
|
|
{ value: "12500", label: "$12,500.00" },
|
|
{ value: "13000", label: "$13,000.00" }, // Replace with your actual values
|
|
];
|
|
|
|
const columns = [
|
|
{
|
|
title: "Product",
|
|
dataIndex: "product",
|
|
render: (text, record) => (
|
|
<span className="productimgname">
|
|
<Link to="/profile" className="product-img stock-img">
|
|
<ImageWithBasePath
|
|
alt={record.name || text || "Product"}
|
|
src={record.productImage || record.image || record.img}
|
|
/>
|
|
</Link>
|
|
<Link to="/profile">{text}</Link>
|
|
</span>
|
|
),
|
|
sorter: (a, b) => a.product.length - b.product.length,
|
|
},
|
|
{
|
|
title: "SKU",
|
|
dataIndex: "sku",
|
|
sorter: (a, b) => a.sku.length - b.sku.length,
|
|
},
|
|
|
|
{
|
|
title: "Category",
|
|
dataIndex: "category",
|
|
sorter: (a, b) => a.category.length - b.category.length,
|
|
},
|
|
|
|
{
|
|
title: "Brand",
|
|
dataIndex: "brand",
|
|
sorter: (a, b) => a.brand.length - b.brand.length,
|
|
},
|
|
{
|
|
title: "Price",
|
|
dataIndex: "price",
|
|
sorter: (a, b) => a.price.length - b.price.length,
|
|
},
|
|
{
|
|
title: "Unit",
|
|
dataIndex: "unit",
|
|
sorter: (a, b) => a.unit.length - b.unit.length,
|
|
},
|
|
{
|
|
title: "Qty",
|
|
dataIndex: "qty",
|
|
sorter: (a, b) => a.qty.length - b.qty.length,
|
|
},
|
|
|
|
{
|
|
title: "Created By",
|
|
dataIndex: "createdby",
|
|
render: (text, record) => (
|
|
<span className="userimgname">
|
|
<Link to="/profile" className="product-img">
|
|
<ImageWithBasePath
|
|
alt={record.createdBy || text || "User"}
|
|
src={record.img || record.avatar || record.userImage}
|
|
/>
|
|
</Link>
|
|
<Link to="/profile">{text}</Link>
|
|
</span>
|
|
),
|
|
sorter: (a, b) => a.createdby.length - b.createdby.length,
|
|
},
|
|
{
|
|
title: "Action",
|
|
dataIndex: "action",
|
|
render: (text, record) => (
|
|
<td className="action-table-data">
|
|
<div className="edit-delete-action">
|
|
<div className="input-block add-lists"></div>
|
|
<Link className="me-2 p-2" to={route.productdetails}>
|
|
<Eye className="feather-view" />
|
|
</Link>
|
|
<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();
|
|
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) {
|
|
handleDeleteProduct(record.id || record.key);
|
|
}
|
|
});
|
|
}}
|
|
>
|
|
<Trash2 className="feather-trash-2" />
|
|
</Link>
|
|
</div>
|
|
</td>
|
|
),
|
|
sorter: (a, b) => a.createdby.length - b.createdby.length,
|
|
},
|
|
];
|
|
const MySwal = withReactContent(Swal);
|
|
|
|
// Removed showConfirmationAlert as we handle confirmation inline
|
|
|
|
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>
|
|
);
|
|
return (
|
|
<div className="page-wrapper">
|
|
<div className="content">
|
|
<div className="page-header">
|
|
<div className="add-item d-flex">
|
|
<div className="page-title">
|
|
<h4>Product List</h4>
|
|
<h6>Manage your products</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={(e) => {
|
|
e.preventDefault();
|
|
dispatch(setToogleHeader(!data));
|
|
}}
|
|
>
|
|
<ChevronUp />
|
|
</Link>
|
|
</OverlayTrigger>
|
|
</li>
|
|
</ul>
|
|
<div className="page-btn">
|
|
<Link to={route.addproduct} className="btn btn-added">
|
|
<PlusCircle className="me-2 iconsize" />
|
|
Add New Product
|
|
</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 Product
|
|
</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"
|
|
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"
|
|
>
|
|
<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={options}
|
|
placeholder="14 09 23"
|
|
/>
|
|
</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">
|
|
<Box className="info-img" />
|
|
<Select
|
|
className="select"
|
|
options={productlist}
|
|
placeholder="Choose Product"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-lg-2 col-sm-6 col-12">
|
|
<div className="input-blocks">
|
|
<StopCircle className="info-img" />
|
|
<Select
|
|
className="select"
|
|
options={categorylist}
|
|
placeholder="Choose Category"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-lg-2 col-sm-6 col-12">
|
|
<div className="input-blocks">
|
|
<GitMerge className="info-img" />
|
|
<Select
|
|
className="select"
|
|
options={subcategorylist}
|
|
placeholder="Choose Sub Category"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-lg-2 col-sm-6 col-12">
|
|
<div className="input-blocks">
|
|
<StopCircle className="info-img" />
|
|
<Select
|
|
className="select"
|
|
options={brandlist}
|
|
placeholder="Nike"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-lg-2 col-sm-6 col-12">
|
|
<div className="input-blocks">
|
|
<i className="fas fa-money-bill info-img" />
|
|
|
|
<Select
|
|
className="select"
|
|
options={price}
|
|
placeholder="Price"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="col-lg-2 col-sm-6 col-12">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
{/* /Filter */}
|
|
<div className="table-responsive">
|
|
{loading ? (
|
|
<div className="text-center p-4">
|
|
<div className="spinner-border text-primary" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p className="mt-2">Loading 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(fetchProducts())}
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<Table columns={columns} dataSource={dataSource} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* /product list */}
|
|
<Brand />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProductList;
|