✨ Add beautiful single-row pagination with total records
- � Beautiful gradient design with animations
- � Total records display with search filtering
- � Single-row layout with Ant Design pagination classes
- � Responsive compact design for all devices
- ⚡ Smooth hover animations and transitions
- � Ultra compact buttons and optimized spacing
- � Real-time search integration with debouncing
- � Glass morphism effects with shimmer animations
This commit is contained in:
parent
9687351177
commit
f75e524565
@ -842,6 +842,13 @@ export const publicRoutes = [
|
||||
element: <EditProduct />,
|
||||
route: Route,
|
||||
},
|
||||
{
|
||||
id: 65.1,
|
||||
path: `${routes.editproduct}/:id`,
|
||||
name: "editproductwithid",
|
||||
element: <EditProduct />,
|
||||
route: Route,
|
||||
},
|
||||
{
|
||||
id: 63,
|
||||
path: routes.videocall,
|
||||
|
||||
@ -4,6 +4,7 @@ const ApiTest = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [result, setResult] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [productId, setProductId] = useState('1'); // Default test ID
|
||||
|
||||
const testApiConnection = async () => {
|
||||
setLoading(true);
|
||||
@ -51,6 +52,50 @@ const ApiTest = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const testProductDetail = async () => {
|
||||
if (!productId) {
|
||||
setError('Please enter a product ID');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
console.log('Testing Product Detail API for ID:', productId);
|
||||
|
||||
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}Products/${productId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setResult(data);
|
||||
console.log('Product Detail Response:', data);
|
||||
} catch (err) {
|
||||
console.error('Product Detail API Error:', err);
|
||||
|
||||
let errorMessage = err.message || 'Failed to fetch product details';
|
||||
|
||||
if (err.name === 'TypeError' && err.message.includes('Failed to fetch')) {
|
||||
errorMessage = 'CORS Error or Network issue';
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="card">
|
||||
@ -60,13 +105,38 @@ const ApiTest = () => {
|
||||
<div className="card-body">
|
||||
<p><strong>API Base URL:</strong> {process.env.REACT_APP_API_BASE_URL}</p>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={testApiConnection}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Testing...' : 'Test API Connection'}
|
||||
</button>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<h6>Test All Products API</h6>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={testApiConnection}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Testing...' : 'Test Products List API'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<h6>Test Product Detail API</h6>
|
||||
<div className="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Enter Product ID"
|
||||
value={productId}
|
||||
onChange={(e) => setProductId(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={testProductDetail}
|
||||
disabled={loading || !productId}
|
||||
>
|
||||
{loading ? 'Testing...' : 'Test Product Detail'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="mt-3">
|
||||
|
||||
@ -6,28 +6,31 @@ const initialState = {
|
||||
totalProducts: 0,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
|
||||
pageSize: 20,
|
||||
hasPrevious: false,
|
||||
hasNext: false,
|
||||
|
||||
// Current product (for edit/view)
|
||||
currentProduct: null,
|
||||
|
||||
|
||||
// Search results
|
||||
searchResults: [],
|
||||
searchQuery: '',
|
||||
|
||||
|
||||
// Categories and brands
|
||||
categories: [],
|
||||
brands: [],
|
||||
|
||||
|
||||
// Loading states
|
||||
loading: false,
|
||||
productLoading: false,
|
||||
searchLoading: false,
|
||||
|
||||
|
||||
// Error states
|
||||
error: null,
|
||||
productError: null,
|
||||
searchError: null,
|
||||
|
||||
|
||||
// Operation states
|
||||
creating: false,
|
||||
updating: false,
|
||||
@ -43,18 +46,27 @@ const productReducer = (state = initialState, action) => {
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS:
|
||||
|
||||
case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS: {
|
||||
// Handle different API response structures
|
||||
const isArrayResponse = Array.isArray(action.payload);
|
||||
const products = isArrayResponse ? action.payload : (action.payload.data || action.payload.items || []);
|
||||
const pagination = action.payload.pagination || {};
|
||||
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
products: action.payload.data || action.payload,
|
||||
totalProducts: action.payload.total || action.payload.length,
|
||||
currentPage: action.payload.currentPage || 1,
|
||||
totalPages: action.payload.totalPages || 1,
|
||||
products: products,
|
||||
totalProducts: pagination.totalCount || action.payload.total || products.length,
|
||||
currentPage: pagination.currentPage || action.payload.currentPage || 1,
|
||||
totalPages: pagination.totalPages || action.payload.totalPages || 1,
|
||||
pageSize: pagination.pageSize || 20,
|
||||
hasPrevious: pagination.hasPrevious || false,
|
||||
hasNext: pagination.hasNext || false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
case PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
@ -69,7 +81,7 @@ const productReducer = (state = initialState, action) => {
|
||||
productLoading: true,
|
||||
productError: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@ -77,7 +89,7 @@ const productReducer = (state = initialState, action) => {
|
||||
currentProduct: action.payload,
|
||||
productError: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
@ -92,7 +104,7 @@ const productReducer = (state = initialState, action) => {
|
||||
creating: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@ -101,7 +113,7 @@ const productReducer = (state = initialState, action) => {
|
||||
totalProducts: state.totalProducts + 1,
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
@ -116,7 +128,7 @@ const productReducer = (state = initialState, action) => {
|
||||
updating: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@ -127,7 +139,7 @@ const productReducer = (state = initialState, action) => {
|
||||
currentProduct: action.payload.data,
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
@ -142,7 +154,7 @@ const productReducer = (state = initialState, action) => {
|
||||
deleting: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@ -151,7 +163,7 @@ const productReducer = (state = initialState, action) => {
|
||||
totalProducts: state.totalProducts - 1,
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
@ -166,7 +178,7 @@ const productReducer = (state = initialState, action) => {
|
||||
searchLoading: true,
|
||||
searchError: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@ -175,7 +187,7 @@ const productReducer = (state = initialState, action) => {
|
||||
searchQuery: action.payload.query || '',
|
||||
searchError: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
@ -189,7 +201,7 @@ const productReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
categories: action.payload,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
@ -204,7 +216,7 @@ const productReducer = (state = initialState, action) => {
|
||||
productError: null,
|
||||
searchError: null,
|
||||
};
|
||||
|
||||
|
||||
case PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import Select from "react-select";
|
||||
import { all_routes } from "../../Router/all_routes";
|
||||
import { DatePicker } from "antd";
|
||||
@ -20,15 +20,90 @@ import {
|
||||
} from "feather-icons-react/build/IconComponents";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setToogleHeader } from "../../core/redux/action";
|
||||
import { fetchProduct } from "../../core/redux/actions/productActions";
|
||||
import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import ImageWithBasePath from "../../core/img/imagewithbasebath";
|
||||
|
||||
const EditProduct = () => {
|
||||
const route = all_routes;
|
||||
const dispatch = useDispatch();
|
||||
const { id } = useParams(); // Get product ID from URL
|
||||
|
||||
const data = useSelector((state) => state.toggle_header);
|
||||
|
||||
// Get product data from Redux store
|
||||
const { currentProduct, productLoading, productError } = useSelector((state) => state.products || {});
|
||||
|
||||
// Track if we've already fetched this product
|
||||
const fetchedProductId = useRef(null);
|
||||
|
||||
// Form state for editing
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
sku: '',
|
||||
description: '',
|
||||
price: '',
|
||||
category: null, // Change to null for Select component
|
||||
brand: null, // Change to null for Select component
|
||||
quantity: '',
|
||||
unit: null, // Change to null for Select component
|
||||
status: '',
|
||||
images: []
|
||||
});
|
||||
|
||||
// Load product data if ID is provided and product not already loaded
|
||||
useEffect(() => {
|
||||
if (id && fetchedProductId.current !== id) {
|
||||
console.log('Fetching product details for ID:', id);
|
||||
fetchedProductId.current = id;
|
||||
dispatch(fetchProduct(id));
|
||||
}
|
||||
}, [id, dispatch]);
|
||||
|
||||
// Helper function to find option by value or label
|
||||
const findSelectOption = (options, value) => {
|
||||
if (!value) return null;
|
||||
return options.find(option =>
|
||||
option.value === value ||
|
||||
option.label === value ||
|
||||
option.value.toLowerCase() === value.toLowerCase() ||
|
||||
option.label.toLowerCase() === value.toLowerCase()
|
||||
) || null;
|
||||
};
|
||||
|
||||
// Update form data when currentProduct changes
|
||||
useEffect(() => {
|
||||
if (currentProduct) {
|
||||
console.log('Product data loaded:', currentProduct);
|
||||
|
||||
// Find matching options for select fields
|
||||
const categoryOption = findSelectOption(category, currentProduct.category || currentProduct.categoryName);
|
||||
const brandOption = findSelectOption(brand, currentProduct.brand || currentProduct.brandName);
|
||||
const unitOption = findSelectOption(unit, currentProduct.unit);
|
||||
|
||||
setFormData({
|
||||
name: currentProduct.name || currentProduct.productName || '',
|
||||
slug: currentProduct.slug || '',
|
||||
sku: currentProduct.sku || currentProduct.code || '',
|
||||
description: currentProduct.description || '',
|
||||
price: currentProduct.price || currentProduct.salePrice || '',
|
||||
category: categoryOption,
|
||||
brand: brandOption,
|
||||
quantity: currentProduct.quantity || currentProduct.stock || '',
|
||||
unit: unitOption,
|
||||
status: currentProduct.status || 'active',
|
||||
images: currentProduct.images || []
|
||||
});
|
||||
|
||||
console.log('Form data updated:', {
|
||||
category: categoryOption,
|
||||
brand: brandOption,
|
||||
unit: unitOption
|
||||
});
|
||||
}
|
||||
}, [currentProduct]);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const handleDateChange = (date) => {
|
||||
setSelectedDate(date);
|
||||
@ -58,6 +133,9 @@ const EditProduct = () => {
|
||||
{ value: "choose", label: "Choose" },
|
||||
{ value: "lenovo", label: "Lenovo" },
|
||||
{ value: "electronics", label: "Electronics" },
|
||||
{ value: "laptop", label: "Laptop" },
|
||||
{ value: "computer", label: "Computer" },
|
||||
{ value: "mobile", label: "Mobile" },
|
||||
];
|
||||
const subcategory = [
|
||||
{ value: "choose", label: "Choose" },
|
||||
@ -78,6 +156,11 @@ const EditProduct = () => {
|
||||
{ value: "choose", label: "Choose" },
|
||||
{ value: "kg", label: "Kg" },
|
||||
{ value: "pc", label: "Pc" },
|
||||
{ value: "pcs", label: "Pcs" },
|
||||
{ value: "piece", label: "Piece" },
|
||||
{ value: "gram", label: "Gram" },
|
||||
{ value: "liter", label: "Liter" },
|
||||
{ value: "meter", label: "Meter" },
|
||||
];
|
||||
const sellingtype = [
|
||||
{ value: "choose", label: "Choose" },
|
||||
@ -114,13 +197,74 @@ const EditProduct = () => {
|
||||
const handleRemoveProduct1 = () => {
|
||||
setIsImageVisible1(false);
|
||||
};
|
||||
// Show loading state
|
||||
if (productLoading) {
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<div className="content">
|
||||
<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 product details...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show error state
|
||||
if (productError) {
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<div className="content">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
<strong>Error:</strong> {productError}
|
||||
<div className="mt-2">
|
||||
<Link to={route.productlist} className="btn btn-secondary me-2">
|
||||
Back to Product List
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
console.log('Retrying fetch for ID:', id);
|
||||
dispatch(fetchProduct(id));
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show not found state
|
||||
if (id && !currentProduct) {
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<div className="content">
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<strong>Product not found!</strong> The product you're trying to edit doesn't exist.
|
||||
<div className="mt-2">
|
||||
<Link to={route.productlist} className="btn btn-secondary">
|
||||
Back to Product List
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-wrapper">
|
||||
<div className="content">
|
||||
<div className="page-header">
|
||||
<div className="add-item d-flex">
|
||||
<div className="page-title">
|
||||
<h4>Edit Product</h4>
|
||||
<h4>Edit Product {currentProduct?.name ? `- ${currentProduct.name}` : ''}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="table-top-head">
|
||||
@ -211,13 +355,33 @@ const EditProduct = () => {
|
||||
<div className="col-lg-4 col-sm-6 col-12">
|
||||
<div className="mb-3 add-product">
|
||||
<label className="form-label">Product Name</label>
|
||||
<input type="text" className="form-control" />
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={formData.name}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
name: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-4 col-sm-6 col-12">
|
||||
<div className="mb-3 add-product">
|
||||
<label className="form-label">Slug</label>
|
||||
<input type="text" className="form-control" />
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={formData.slug}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
slug: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-4 col-sm-6 col-12">
|
||||
@ -227,6 +391,13 @@ const EditProduct = () => {
|
||||
type="text"
|
||||
className="form-control list"
|
||||
placeholder="Enter SKU"
|
||||
value={formData.sku}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
sku: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<Link
|
||||
to={route.addproduct}
|
||||
@ -255,7 +426,15 @@ const EditProduct = () => {
|
||||
<Select
|
||||
className="select"
|
||||
options={category}
|
||||
placeholder="Lenovo"
|
||||
placeholder="Choose Category"
|
||||
value={formData.category}
|
||||
onChange={(selectedOption) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
category: selectedOption
|
||||
}));
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -301,7 +480,15 @@ const EditProduct = () => {
|
||||
<Select
|
||||
className="select"
|
||||
options={brand}
|
||||
placeholder="Nike"
|
||||
placeholder="Choose Brand"
|
||||
value={formData.brand}
|
||||
onChange={(selectedOption) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
brand: selectedOption
|
||||
}));
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -321,7 +508,15 @@ const EditProduct = () => {
|
||||
<Select
|
||||
className="select"
|
||||
options={unit}
|
||||
placeholder="Kg"
|
||||
placeholder="Choose Unit"
|
||||
value={formData.unit}
|
||||
onChange={(selectedOption) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
unit: selectedOption
|
||||
}));
|
||||
}}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -374,7 +569,13 @@ const EditProduct = () => {
|
||||
<textarea
|
||||
className="form-control h-100"
|
||||
rows={5}
|
||||
defaultValue={""}
|
||||
value={formData.description}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
description: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1">Maximum 60 Characters</p>
|
||||
</div>
|
||||
@ -474,13 +675,33 @@ const EditProduct = () => {
|
||||
<div className="col-lg-4 col-sm-6 col-12">
|
||||
<div className="input-blocks add-product">
|
||||
<label>Quantity</label>
|
||||
<input type="text" className="form-control" />
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={formData.quantity}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
quantity: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-4 col-sm-6 col-12">
|
||||
<div className="input-blocks add-product">
|
||||
<label>Price</label>
|
||||
<input type="text" className="form-control" />
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={formData.price}
|
||||
onChange={(e) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
price: e.target.value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-4 col-sm-6 col-12">
|
||||
|
||||
@ -31,12 +31,54 @@ import {
|
||||
clearProductError
|
||||
} from "../../core/redux/actions/productActions";
|
||||
|
||||
// Add CSS animations for beautiful UI
|
||||
const shimmerKeyframes = `
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% { box-shadow: 0 0 5px rgba(52, 152, 219, 0.3); }
|
||||
50% { box-shadow: 0 0 20px rgba(52, 152, 219, 0.6); }
|
||||
}
|
||||
`;
|
||||
|
||||
// Inject CSS into head if not already present
|
||||
if (typeof document !== 'undefined' && !document.getElementById('beautiful-pagination-styles')) {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.id = 'beautiful-pagination-styles';
|
||||
styleSheet.type = 'text/css';
|
||||
styleSheet.innerText = shimmerKeyframes;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
const ProductList = () => {
|
||||
// Use new Redux structure for API data, fallback to legacy for existing functionality
|
||||
const {
|
||||
products: apiProducts,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
totalProducts,
|
||||
totalPages,
|
||||
pageSize: reduxPageSize,
|
||||
currentPage: reduxCurrentPage
|
||||
} = useSelector((state) => state.products);
|
||||
|
||||
// Fallback to legacy data if API data is not available
|
||||
@ -49,26 +91,46 @@ const ProductList = () => {
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// State for pagination - sync with Redux
|
||||
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
|
||||
const pageSize = reduxPageSize || 20;
|
||||
|
||||
// Debounced search term
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||
|
||||
const toggleFilterVisibility = () => {
|
||||
setIsFilterVisible((prevVisibility) => !prevVisibility);
|
||||
};
|
||||
|
||||
const route = all_routes;
|
||||
|
||||
// Fetch products on component mount
|
||||
// Debounce search term
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
}, 500); // 500ms delay
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
// Fetch products when debounced search term or pagination changes
|
||||
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
|
||||
const searchParams = {
|
||||
page: currentPage,
|
||||
pageSize: pageSize,
|
||||
searchTerm: debouncedSearchTerm
|
||||
};
|
||||
|
||||
await dispatch(fetchProducts(searchParams));
|
||||
} catch (error) {
|
||||
console.error('Failed to load products:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadProducts();
|
||||
}, [dispatch]);
|
||||
}, [dispatch, currentPage, pageSize, debouncedSearchTerm]);
|
||||
|
||||
// Handle product deletion
|
||||
const handleDeleteProduct = async (productId) => {
|
||||
@ -102,10 +164,25 @@ const ProductList = () => {
|
||||
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
|
||||
// Reset to first page when searching
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Handle pagination
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
// Calculate pagination info
|
||||
const totalRecords = totalProducts || dataSource.length;
|
||||
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
|
||||
const actualTotalPages = totalPages || calculatedTotalPages;
|
||||
|
||||
const startRecord = totalRecords > 0 ? (currentPage - 1) * pageSize + 1 : 0;
|
||||
const endRecord = Math.min(currentPage * pageSize, totalRecords);
|
||||
|
||||
// Debug logs removed for production
|
||||
|
||||
// Clear error when component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@ -163,34 +240,83 @@ const ProductList = () => {
|
||||
{
|
||||
title: "SKU",
|
||||
dataIndex: "sku",
|
||||
sorter: (a, b) => a.sku.length - b.sku.length,
|
||||
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: "Category",
|
||||
dataIndex: "category",
|
||||
sorter: (a, b) => a.category.length - b.category.length,
|
||||
render: (_, record) => {
|
||||
const category = record.category || record.categoryName || '-';
|
||||
return <span>{category}</span>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
const catA = a.category || a.categoryName || '';
|
||||
const catB = b.category || b.categoryName || '';
|
||||
return catA.length - catB.length;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: "Brand",
|
||||
dataIndex: "brand",
|
||||
sorter: (a, b) => a.brand.length - b.brand.length,
|
||||
render: (_, record) => {
|
||||
const brand = record.brand || record.brandName || '-';
|
||||
return <span>{brand}</span>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
const brandA = a.brand || a.brandName || '';
|
||||
const brandB = b.brand || b.brandName || '';
|
||||
return brandA.length - brandB.length;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Price",
|
||||
dataIndex: "price",
|
||||
sorter: (a, b) => a.price.length - b.price.length,
|
||||
render: (_, record) => {
|
||||
const price = record.price || record.salePrice || record.unitPrice || 0;
|
||||
return <span>${Number(price).toFixed(2)}</span>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
const priceA = Number(a.price || a.salePrice || a.unitPrice || 0);
|
||||
const priceB = Number(b.price || b.salePrice || b.unitPrice || 0);
|
||||
return priceA - priceB;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Unit",
|
||||
dataIndex: "unit",
|
||||
sorter: (a, b) => a.unit.length - b.unit.length,
|
||||
render: (_, record) => {
|
||||
const unit = record.unit || record.unitOfMeasure || '-';
|
||||
return <span>{unit}</span>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
const unitA = a.unit || a.unitOfMeasure || '';
|
||||
const unitB = b.unit || b.unitOfMeasure || '';
|
||||
return unitA.length - unitB.length;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Qty",
|
||||
dataIndex: "qty",
|
||||
sorter: (a, b) => a.qty.length - b.qty.length,
|
||||
render: (_, record) => {
|
||||
// Try multiple possible field names for quantity
|
||||
const quantity = record.qty || record.quantity || record.stock || record.stockQuantity || 0;
|
||||
return <span>{quantity}</span>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
const qtyA = a.qty || a.quantity || a.stock || a.stockQuantity || 0;
|
||||
const qtyB = b.qty || b.quantity || b.stock || b.stockQuantity || 0;
|
||||
return Number(qtyA) - Number(qtyB);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@ -509,7 +635,250 @@ const ProductList = () => {
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Table columns={columns} dataSource={dataSource} />
|
||||
<>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
pagination={false} // Disable Ant Design pagination
|
||||
/>
|
||||
|
||||
{/* Ant Design Pagination Structure with Beautiful Design */}
|
||||
<div
|
||||
className="ant-pagination ant-table-pagination ant-table-pagination-right css-dev-only-do-not-override-vrrzze"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)',
|
||||
border: '1px solid rgba(52, 152, 219, 0.3)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
transition: 'all 0.3s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
padding: '16px 24px',
|
||||
margin: '16px 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 12px 40px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(52, 152, 219, 0.2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(52, 152, 219, 0.1)';
|
||||
}}
|
||||
>
|
||||
{/* Animated background glow */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(52, 152, 219, 0.1), transparent)',
|
||||
animation: 'shimmer 3s infinite',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Left side - Total Records Info */}
|
||||
<div
|
||||
className="ant-pagination-total-text"
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'linear-gradient(45deg, #3498db, #2ecc71)',
|
||||
borderRadius: '50%',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px',
|
||||
boxShadow: '0 4px 12px rgba(52, 152, 219, 0.3)'
|
||||
}}
|
||||
>
|
||||
📊
|
||||
</div>
|
||||
<div>
|
||||
<span style={{color: '#bdc3c7', fontSize: '14px', lineHeight: '1.4'}}>
|
||||
Showing <strong style={{color: '#3498db', fontWeight: '700'}}>{startRecord}</strong> to <strong style={{color: '#3498db', fontWeight: '700'}}>{endRecord}</strong> of <strong style={{color: '#e74c3c', fontWeight: '700'}}>{totalRecords}</strong> entries
|
||||
{debouncedSearchTerm && (
|
||||
<div style={{color: '#2ecc71', fontSize: '12px', marginTop: '2px'}}>
|
||||
🔍 Filtered from <strong style={{color: '#f39c12'}}>{totalProducts || totalRecords}</strong> total products
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Pagination Controls */}
|
||||
{actualTotalPages > 1 && (
|
||||
<div
|
||||
className="ant-pagination-options"
|
||||
style={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
<span style={{color: '#bdc3c7', fontSize: '12px', marginRight: '8px'}}>
|
||||
Page {currentPage} of {actualTotalPages}
|
||||
</span>
|
||||
<ul
|
||||
className="ant-pagination-list"
|
||||
style={{
|
||||
display: 'flex',
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
gap: '4px'
|
||||
}}
|
||||
>
|
||||
<li className={`ant-pagination-prev ${currentPage === 1 ? 'ant-pagination-disabled' : ''}`}>
|
||||
<button
|
||||
className="ant-pagination-item-link"
|
||||
style={{
|
||||
background: currentPage === 1
|
||||
? 'rgba(52, 73, 94, 0.5)'
|
||||
: 'linear-gradient(45deg, #3498db, #2980b9)',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: currentPage === 1 ? '#7f8c8d' : '#ffffff',
|
||||
padding: '6px 12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: currentPage === 1
|
||||
? 'none'
|
||||
: '0 2px 8px rgba(52, 152, 219, 0.3)',
|
||||
cursor: currentPage === 1 ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 1) {
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 1) {
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
e.target.style.boxShadow = '0 2px 8px rgba(52, 152, 219, 0.3)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{Array.from({ length: Math.min(3, actualTotalPages) }, (_, i) => {
|
||||
let pageNum = i + 1;
|
||||
if (actualTotalPages > 3 && currentPage > 2) {
|
||||
pageNum = currentPage - 1 + i;
|
||||
}
|
||||
|
||||
const isActive = currentPage === pageNum;
|
||||
|
||||
return (
|
||||
<li key={pageNum} className={`ant-pagination-item ${isActive ? 'ant-pagination-item-active' : ''}`}>
|
||||
<button
|
||||
className="ant-pagination-item-link"
|
||||
style={{
|
||||
background: isActive
|
||||
? 'linear-gradient(45deg, #e74c3c, #c0392b)'
|
||||
: 'linear-gradient(45deg, #34495e, #2c3e50)',
|
||||
border: isActive
|
||||
? '2px solid #e74c3c'
|
||||
: '1px solid rgba(52, 152, 219, 0.3)',
|
||||
borderRadius: '8px',
|
||||
color: '#ffffff',
|
||||
padding: '6px 12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '700',
|
||||
minWidth: '32px',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: isActive
|
||||
? '0 4px 12px rgba(231, 76, 60, 0.4)'
|
||||
: '0 2px 8px rgba(52, 73, 94, 0.3)',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.target.style.background = 'linear-gradient(45deg, #3498db, #2980b9)';
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.target.style.background = 'linear-gradient(45deg, #34495e, #2c3e50)';
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
e.target.style.boxShadow = '0 2px 8px rgba(52, 73, 94, 0.3)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li className={`ant-pagination-next ${currentPage === actualTotalPages ? 'ant-pagination-disabled' : ''}`}>
|
||||
<button
|
||||
className="ant-pagination-item-link"
|
||||
style={{
|
||||
background: currentPage === actualTotalPages
|
||||
? 'rgba(52, 73, 94, 0.5)'
|
||||
: 'linear-gradient(45deg, #3498db, #2980b9)',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: currentPage === actualTotalPages ? '#7f8c8d' : '#ffffff',
|
||||
padding: '6px 12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: currentPage === actualTotalPages
|
||||
? 'none'
|
||||
: '0 2px 8px rgba(52, 152, 219, 0.3)',
|
||||
cursor: currentPage === actualTotalPages ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage === actualTotalPages}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== actualTotalPages) {
|
||||
e.target.style.transform = 'translateY(-1px)';
|
||||
e.target.style.boxShadow = '0 4px 12px rgba(52, 152, 219, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== actualTotalPages) {
|
||||
e.target.style.transform = 'translateY(0)';
|
||||
e.target.style.boxShadow = '0 2px 8px rgba(52, 152, 219, 0.3)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user