update
This commit is contained in:
parent
6b6e2bbc8a
commit
9687351177
3
.env
Normal file
3
.env
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
REACT_APP_API_BASE_URL=https://trantran.zenstores.com.vn/api/
|
||||||
|
# CORS Proxy for development (uncomment if needed)
|
||||||
|
# REACT_APP_API_BASE_URL=https://cors-anywhere.herokuapp.com/https://trantran.zenstores.com.vn/api/
|
||||||
225
API_INTEGRATION_README.md
Normal file
225
API_INTEGRATION_README.md
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
# Products API Integration
|
||||||
|
|
||||||
|
This document describes the integration of the Products API from `https://trantran.zenstores.com.vn/api/Products` into the React POS application.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The integration includes:
|
||||||
|
- Environment configuration for API base URL
|
||||||
|
- Axios-based API service layer
|
||||||
|
- Redux actions and reducers for state management
|
||||||
|
- Updated ProductList component with API integration
|
||||||
|
- Error handling and loading states
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
### New Files Created:
|
||||||
|
|
||||||
|
1. **`.env`** - Environment variables
|
||||||
|
```
|
||||||
|
REACT_APP_API_BASE_URL=https://trantran.zenstores.com.vn/api/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **`src/services/api.js`** - Main API configuration with axios
|
||||||
|
- Base axios instance with interceptors
|
||||||
|
- Request/response logging
|
||||||
|
- Error handling
|
||||||
|
- Authentication token support
|
||||||
|
|
||||||
|
3. **`src/services/productsApi.js`** - Products API service
|
||||||
|
- getAllProducts()
|
||||||
|
- getProductById(id)
|
||||||
|
- createProduct(productData)
|
||||||
|
- updateProduct(id, productData)
|
||||||
|
- deleteProduct(id)
|
||||||
|
- searchProducts(query)
|
||||||
|
- getCategories()
|
||||||
|
- getBrands()
|
||||||
|
- Bulk operations
|
||||||
|
|
||||||
|
4. **`src/core/redux/actions/productActions.js`** - Redux actions
|
||||||
|
- Async actions for all CRUD operations
|
||||||
|
- Loading states management
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
5. **`src/core/redux/reducers/productReducer.js`** - Redux reducer
|
||||||
|
- State management for products
|
||||||
|
- Loading and error states
|
||||||
|
- Search functionality
|
||||||
|
|
||||||
|
6. **`src/components/ApiTest.jsx`** - API testing component
|
||||||
|
- Test API connectivity
|
||||||
|
- Debug API responses
|
||||||
|
|
||||||
|
### Modified Files:
|
||||||
|
|
||||||
|
1. **`src/core/redux/reducer.jsx`** - Updated to use combineReducers
|
||||||
|
- Added new product reducer
|
||||||
|
- Maintained backward compatibility with legacy reducer
|
||||||
|
|
||||||
|
2. **`src/feature-module/inventory/productlist.jsx`** - Enhanced with API integration
|
||||||
|
- Fetches data from API on component mount
|
||||||
|
- Fallback to legacy data if API fails
|
||||||
|
- Loading states and error handling
|
||||||
|
- Real-time search functionality
|
||||||
|
- Delete confirmation with API calls
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
1. Ensure the `.env` file is in the project root with:
|
||||||
|
```
|
||||||
|
REACT_APP_API_BASE_URL=https://trantran.zenstores.com.vn/api/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Restart the development server after adding environment variables.
|
||||||
|
|
||||||
|
### Using the API Service
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { productsApi } from '../services/productsApi';
|
||||||
|
|
||||||
|
// Get all products
|
||||||
|
const products = await productsApi.getAllProducts();
|
||||||
|
|
||||||
|
// Get product by ID
|
||||||
|
const product = await productsApi.getProductById(123);
|
||||||
|
|
||||||
|
// Create new product
|
||||||
|
const newProduct = await productsApi.createProduct({
|
||||||
|
name: 'Product Name',
|
||||||
|
price: 99.99,
|
||||||
|
// ... other fields
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update product
|
||||||
|
const updatedProduct = await productsApi.updateProduct(123, {
|
||||||
|
name: 'Updated Name',
|
||||||
|
price: 149.99
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete product
|
||||||
|
await productsApi.deleteProduct(123);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Redux Actions
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { fetchProducts, createProduct, deleteProduct } from '../core/redux/actions/productActions';
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { products, loading, error } = useSelector(state => state.products);
|
||||||
|
|
||||||
|
// Fetch products
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchProducts());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
// Create product
|
||||||
|
const handleCreate = async (productData) => {
|
||||||
|
try {
|
||||||
|
await dispatch(createProduct(productData));
|
||||||
|
// Success handling
|
||||||
|
} catch (error) {
|
||||||
|
// Error handling
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete product
|
||||||
|
const handleDelete = async (productId) => {
|
||||||
|
try {
|
||||||
|
await dispatch(deleteProduct(productId));
|
||||||
|
// Success handling
|
||||||
|
} catch (error) {
|
||||||
|
// Error handling
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{loading && <div>Loading...</div>}
|
||||||
|
{error && <div>Error: {error}</div>}
|
||||||
|
{products.map(product => (
|
||||||
|
<div key={product.id}>{product.name}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The integration supports the following endpoints:
|
||||||
|
|
||||||
|
- `GET /Products` - Get all products
|
||||||
|
- `GET /Products/{id}` - Get product by ID
|
||||||
|
- `POST /Products` - Create new product
|
||||||
|
- `PUT /Products/{id}` - Update product
|
||||||
|
- `DELETE /Products/{id}` - Delete product
|
||||||
|
- `GET /Products/search?q={query}` - Search products
|
||||||
|
- `GET /Products/categories` - Get categories
|
||||||
|
- `GET /Products/brands` - Get brands
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The integration includes comprehensive error handling:
|
||||||
|
|
||||||
|
1. **Network Errors** - Connection issues, timeouts
|
||||||
|
2. **HTTP Errors** - 4xx, 5xx status codes
|
||||||
|
3. **Authentication Errors** - 401 Unauthorized
|
||||||
|
4. **Validation Errors** - Invalid data format
|
||||||
|
|
||||||
|
Errors are displayed to users with retry options where appropriate.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use the `ApiTest` component to verify API connectivity:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import ApiTest from './components/ApiTest';
|
||||||
|
|
||||||
|
// Add to your router or render directly
|
||||||
|
<ApiTest />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
The integration maintains backward compatibility:
|
||||||
|
- Legacy Redux state structure is preserved
|
||||||
|
- Components fallback to static data if API fails
|
||||||
|
- Existing functionality continues to work
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Authentication** - Add user authentication if required
|
||||||
|
2. **Caching** - Implement data caching for better performance
|
||||||
|
3. **Pagination** - Add pagination support for large datasets
|
||||||
|
4. **Real-time Updates** - Consider WebSocket integration for live updates
|
||||||
|
5. **Offline Support** - Add offline functionality with local storage
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues:
|
||||||
|
|
||||||
|
1. **CORS Errors** - Ensure the API server allows requests from your domain
|
||||||
|
2. **Environment Variables** - Restart development server after changing .env
|
||||||
|
3. **Network Issues** - Check API server availability
|
||||||
|
4. **Authentication** - Verify API key/token if required
|
||||||
|
|
||||||
|
### Debug Steps:
|
||||||
|
|
||||||
|
1. Check browser console for errors
|
||||||
|
2. Use the ApiTest component to verify connectivity
|
||||||
|
3. Check Network tab in browser DevTools
|
||||||
|
4. Verify environment variables are loaded correctly
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues related to the API integration, check:
|
||||||
|
1. Browser console for error messages
|
||||||
|
2. Network requests in DevTools
|
||||||
|
3. Redux DevTools for state changes
|
||||||
|
4. API server logs if accessible
|
||||||
@ -193,6 +193,7 @@ import TaxRates from "../feature-module/settings/financialsettings/taxrates";
|
|||||||
import CurrencySettings from "../feature-module/settings/financialsettings/currencysettings";
|
import CurrencySettings from "../feature-module/settings/financialsettings/currencysettings";
|
||||||
import WareHouses from "../core/modals/peoples/warehouses";
|
import WareHouses from "../core/modals/peoples/warehouses";
|
||||||
import Coupons from "../feature-module/coupons/coupons";
|
import Coupons from "../feature-module/coupons/coupons";
|
||||||
|
import ApiTest from "../components/ApiTest";
|
||||||
import { all_routes } from "./all_routes";
|
import { all_routes } from "./all_routes";
|
||||||
export const publicRoutes = [
|
export const publicRoutes = [
|
||||||
{
|
{
|
||||||
@ -1382,13 +1383,20 @@ export const publicRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 115,
|
id: 115,
|
||||||
|
path: "/api-test",
|
||||||
|
name: "apitest",
|
||||||
|
element: <ApiTest />,
|
||||||
|
route: Route,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 116,
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "Root",
|
name: "Root",
|
||||||
element: <Navigate to="/" />,
|
element: <Navigate to="/" />,
|
||||||
route: Route,
|
route: Route,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 116,
|
id: 117,
|
||||||
path: "*",
|
path: "*",
|
||||||
name: "NotFound",
|
name: "NotFound",
|
||||||
element: <Navigate to="/" />,
|
element: <Navigate to="/" />,
|
||||||
|
|||||||
102
src/components/ApiTest.jsx
Normal file
102
src/components/ApiTest.jsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ApiTest = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const testApiConnection = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Testing API connection to:', process.env.REACT_APP_API_BASE_URL);
|
||||||
|
|
||||||
|
// Test with fetch first to get more detailed error info
|
||||||
|
const response = await fetch(`${process.env.REACT_APP_API_BASE_URL}Products`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response headers:', response.headers);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setResult(data);
|
||||||
|
console.log('API Response:', data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API Error Details:', {
|
||||||
|
message: err.message,
|
||||||
|
name: err.name,
|
||||||
|
stack: err.stack,
|
||||||
|
cause: err.cause
|
||||||
|
});
|
||||||
|
|
||||||
|
let errorMessage = err.message || 'Failed to connect to API';
|
||||||
|
|
||||||
|
if (err.name === 'TypeError' && err.message.includes('Failed to fetch')) {
|
||||||
|
errorMessage = 'CORS Error: API server may not allow requests from this domain, or server is unreachable';
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-4">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h5>API Connection Test</h5>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="spinner-border text-primary" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger mt-3">
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="alert alert-success mt-3">
|
||||||
|
<strong>Success!</strong> API connection working.
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary>Response Data</summary>
|
||||||
|
<pre className="mt-2" style={{ fontSize: '12px', maxHeight: '300px', overflow: 'auto' }}>
|
||||||
|
{JSON.stringify(result, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiTest;
|
||||||
@ -6,7 +6,12 @@ const ImageWithBasePath = (props) => {
|
|||||||
// Combine the base path and the provided src to create the full image source URL
|
// Combine the base path and the provided src to create the full image source URL
|
||||||
// Handle both relative and absolute paths
|
// Handle both relative and absolute paths
|
||||||
let fullSrc;
|
let fullSrc;
|
||||||
if (props.src.startsWith('http')) {
|
|
||||||
|
// Check if src is provided and is a string
|
||||||
|
if (!props.src || typeof props.src !== 'string') {
|
||||||
|
// Use a default placeholder image if src is undefined or invalid
|
||||||
|
fullSrc = `${base_path}assets/img/placeholder.png`;
|
||||||
|
} else if (props.src.startsWith('http')) {
|
||||||
fullSrc = props.src;
|
fullSrc = props.src;
|
||||||
} else {
|
} else {
|
||||||
// Ensure there's always a slash between base_path and src
|
// Ensure there's always a slash between base_path and src
|
||||||
@ -38,7 +43,7 @@ const ImageWithBasePath = (props) => {
|
|||||||
// Add PropTypes validation
|
// Add PropTypes validation
|
||||||
ImageWithBasePath.propTypes = {
|
ImageWithBasePath.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
src: PropTypes.string.isRequired, // Make 'src' required
|
src: PropTypes.string, // Allow src to be optional
|
||||||
alt: PropTypes.string,
|
alt: PropTypes.string,
|
||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
|
|||||||
204
src/core/redux/actions/productActions.js
Normal file
204
src/core/redux/actions/productActions.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { productsApi } from '../../../services/productsApi';
|
||||||
|
|
||||||
|
// Action Types
|
||||||
|
export const PRODUCT_ACTIONS = {
|
||||||
|
// Fetch Products
|
||||||
|
FETCH_PRODUCTS_REQUEST: 'FETCH_PRODUCTS_REQUEST',
|
||||||
|
FETCH_PRODUCTS_SUCCESS: 'FETCH_PRODUCTS_SUCCESS',
|
||||||
|
FETCH_PRODUCTS_FAILURE: 'FETCH_PRODUCTS_FAILURE',
|
||||||
|
|
||||||
|
// Fetch Single Product
|
||||||
|
FETCH_PRODUCT_REQUEST: 'FETCH_PRODUCT_REQUEST',
|
||||||
|
FETCH_PRODUCT_SUCCESS: 'FETCH_PRODUCT_SUCCESS',
|
||||||
|
FETCH_PRODUCT_FAILURE: 'FETCH_PRODUCT_FAILURE',
|
||||||
|
|
||||||
|
// Create Product
|
||||||
|
CREATE_PRODUCT_REQUEST: 'CREATE_PRODUCT_REQUEST',
|
||||||
|
CREATE_PRODUCT_SUCCESS: 'CREATE_PRODUCT_SUCCESS',
|
||||||
|
CREATE_PRODUCT_FAILURE: 'CREATE_PRODUCT_FAILURE',
|
||||||
|
|
||||||
|
// Update Product
|
||||||
|
UPDATE_PRODUCT_REQUEST: 'UPDATE_PRODUCT_REQUEST',
|
||||||
|
UPDATE_PRODUCT_SUCCESS: 'UPDATE_PRODUCT_SUCCESS',
|
||||||
|
UPDATE_PRODUCT_FAILURE: 'UPDATE_PRODUCT_FAILURE',
|
||||||
|
|
||||||
|
// Delete Product
|
||||||
|
DELETE_PRODUCT_REQUEST: 'DELETE_PRODUCT_REQUEST',
|
||||||
|
DELETE_PRODUCT_SUCCESS: 'DELETE_PRODUCT_SUCCESS',
|
||||||
|
DELETE_PRODUCT_FAILURE: 'DELETE_PRODUCT_FAILURE',
|
||||||
|
|
||||||
|
// Search Products
|
||||||
|
SEARCH_PRODUCTS_REQUEST: 'SEARCH_PRODUCTS_REQUEST',
|
||||||
|
SEARCH_PRODUCTS_SUCCESS: 'SEARCH_PRODUCTS_SUCCESS',
|
||||||
|
SEARCH_PRODUCTS_FAILURE: 'SEARCH_PRODUCTS_FAILURE',
|
||||||
|
|
||||||
|
// Categories and Brands
|
||||||
|
FETCH_CATEGORIES_SUCCESS: 'FETCH_CATEGORIES_SUCCESS',
|
||||||
|
FETCH_BRANDS_SUCCESS: 'FETCH_BRANDS_SUCCESS',
|
||||||
|
|
||||||
|
// Clear States
|
||||||
|
CLEAR_PRODUCT_ERROR: 'CLEAR_PRODUCT_ERROR',
|
||||||
|
CLEAR_CURRENT_PRODUCT: 'CLEAR_CURRENT_PRODUCT',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
// Fetch all products
|
||||||
|
export const fetchProducts = (params = {}) => async (dispatch) => {
|
||||||
|
dispatch({ type: PRODUCT_ACTIONS.FETCH_PRODUCTS_REQUEST });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await productsApi.getAllProducts(params);
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS,
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE,
|
||||||
|
payload: error.response?.data?.message || error.message || 'Failed to fetch products',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch single product
|
||||||
|
export const fetchProduct = (id) => async (dispatch) => {
|
||||||
|
dispatch({ type: PRODUCT_ACTIONS.FETCH_PRODUCT_REQUEST });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await productsApi.getProductById(id);
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS,
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE,
|
||||||
|
payload: error.response?.data?.message || error.message || 'Failed to fetch product',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create product
|
||||||
|
export const createProduct = (productData) => async (dispatch) => {
|
||||||
|
dispatch({ type: PRODUCT_ACTIONS.CREATE_PRODUCT_REQUEST });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await productsApi.createProduct(productData);
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS,
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE,
|
||||||
|
payload: error.response?.data?.message || error.message || 'Failed to create product',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update product
|
||||||
|
export const updateProduct = (id, productData) => async (dispatch) => {
|
||||||
|
dispatch({ type: PRODUCT_ACTIONS.UPDATE_PRODUCT_REQUEST });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await productsApi.updateProduct(id, productData);
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS,
|
||||||
|
payload: { id, data },
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE,
|
||||||
|
payload: error.response?.data?.message || error.message || 'Failed to update product',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete product
|
||||||
|
export const deleteProduct = (id) => async (dispatch) => {
|
||||||
|
dispatch({ type: PRODUCT_ACTIONS.DELETE_PRODUCT_REQUEST });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await productsApi.deleteProduct(id);
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS,
|
||||||
|
payload: id,
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE,
|
||||||
|
payload: error.response?.data?.message || error.message || 'Failed to delete product',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search products
|
||||||
|
export const searchProducts = (query, params = {}) => async (dispatch) => {
|
||||||
|
dispatch({ type: PRODUCT_ACTIONS.SEARCH_PRODUCTS_REQUEST });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await productsApi.searchProducts(query, params);
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS,
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE,
|
||||||
|
payload: error.response?.data?.message || error.message || 'Failed to search products',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch categories
|
||||||
|
export const fetchCategories = () => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const data = await productsApi.getCategories();
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.FETCH_CATEGORIES_SUCCESS,
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch categories:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch brands
|
||||||
|
export const fetchBrands = () => async (dispatch) => {
|
||||||
|
try {
|
||||||
|
const data = await productsApi.getBrands();
|
||||||
|
dispatch({
|
||||||
|
type: PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS,
|
||||||
|
payload: data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch brands:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear error
|
||||||
|
export const clearProductError = () => ({
|
||||||
|
type: PRODUCT_ACTIONS.CLEAR_PRODUCT_ERROR,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear current product
|
||||||
|
export const clearCurrentProduct = () => ({
|
||||||
|
type: PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT,
|
||||||
|
});
|
||||||
@ -1,6 +1,9 @@
|
|||||||
|
import { combineReducers } from '@reduxjs/toolkit';
|
||||||
import initialState from "./initial.value";
|
import initialState from "./initial.value";
|
||||||
|
import productReducer from './reducers/productReducer';
|
||||||
|
|
||||||
const rootReducer = (state = initialState, action) => {
|
// Legacy reducer for existing functionality
|
||||||
|
const legacyReducer = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "Product_list":
|
case "Product_list":
|
||||||
return { ...state, product_list: action.payload };
|
return { ...state, product_list: action.payload };
|
||||||
@ -66,4 +69,10 @@ const rootReducer = (state = initialState, action) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Combine reducers
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
legacy: legacyReducer,
|
||||||
|
products: productReducer,
|
||||||
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|||||||
220
src/core/redux/reducers/productReducer.js
Normal file
220
src/core/redux/reducers/productReducer.js
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import { PRODUCT_ACTIONS } from '../actions/productActions';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
// Products list
|
||||||
|
products: [],
|
||||||
|
totalProducts: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
deleting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const productReducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
// Fetch Products
|
||||||
|
case PRODUCT_ACTIONS.FETCH_PRODUCTS_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.FETCH_PRODUCTS_SUCCESS:
|
||||||
|
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,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.FETCH_PRODUCTS_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
error: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch Single Product
|
||||||
|
case PRODUCT_ACTIONS.FETCH_PRODUCT_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
productLoading: true,
|
||||||
|
productError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.FETCH_PRODUCT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
productLoading: false,
|
||||||
|
currentProduct: action.payload,
|
||||||
|
productError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.FETCH_PRODUCT_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
productLoading: false,
|
||||||
|
productError: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create Product
|
||||||
|
case PRODUCT_ACTIONS.CREATE_PRODUCT_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
creating: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.CREATE_PRODUCT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
creating: false,
|
||||||
|
products: [action.payload, ...state.products],
|
||||||
|
totalProducts: state.totalProducts + 1,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.CREATE_PRODUCT_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
creating: false,
|
||||||
|
error: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update Product
|
||||||
|
case PRODUCT_ACTIONS.UPDATE_PRODUCT_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
updating: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.UPDATE_PRODUCT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
updating: false,
|
||||||
|
products: state.products.map(product =>
|
||||||
|
product.id === action.payload.id ? action.payload.data : product
|
||||||
|
),
|
||||||
|
currentProduct: action.payload.data,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.UPDATE_PRODUCT_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
updating: false,
|
||||||
|
error: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete Product
|
||||||
|
case PRODUCT_ACTIONS.DELETE_PRODUCT_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deleting: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.DELETE_PRODUCT_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deleting: false,
|
||||||
|
products: state.products.filter(product => product.id !== action.payload),
|
||||||
|
totalProducts: state.totalProducts - 1,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.DELETE_PRODUCT_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deleting: false,
|
||||||
|
error: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search Products
|
||||||
|
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_REQUEST:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searchLoading: true,
|
||||||
|
searchError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searchLoading: false,
|
||||||
|
searchResults: action.payload.data || action.payload,
|
||||||
|
searchQuery: action.payload.query || '',
|
||||||
|
searchError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.SEARCH_PRODUCTS_FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
searchLoading: false,
|
||||||
|
searchError: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Categories and Brands
|
||||||
|
case PRODUCT_ACTIONS.FETCH_CATEGORIES_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
categories: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.FETCH_BRANDS_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
brands: action.payload,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear States
|
||||||
|
case PRODUCT_ACTIONS.CLEAR_PRODUCT_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
productError: null,
|
||||||
|
searchError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case PRODUCT_ACTIONS.CLEAR_CURRENT_PRODUCT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentProduct: null,
|
||||||
|
productError: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default productReducer;
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
StopCircle,
|
StopCircle,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "feather-icons-react/build/IconComponents";
|
} from "feather-icons-react/build/IconComponents";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Select from "react-select";
|
import Select from "react-select";
|
||||||
@ -24,17 +24,94 @@ import { OverlayTrigger, Tooltip } from "react-bootstrap";
|
|||||||
import Table from "../../core/pagination/datatable";
|
import Table from "../../core/pagination/datatable";
|
||||||
import { setToogleHeader } from "../../core/redux/action";
|
import { setToogleHeader } from "../../core/redux/action";
|
||||||
import { Download } from "react-feather";
|
import { Download } from "react-feather";
|
||||||
|
import {
|
||||||
|
fetchProducts,
|
||||||
|
fetchProduct,
|
||||||
|
deleteProduct,
|
||||||
|
clearProductError
|
||||||
|
} from "../../core/redux/actions/productActions";
|
||||||
|
|
||||||
const ProductList = () => {
|
const ProductList = () => {
|
||||||
const dataSource = useSelector((state) => state.product_list);
|
// 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 dispatch = useDispatch();
|
||||||
const data = useSelector((state) => state.toggle_header);
|
const data = useSelector((state) => state.legacy?.toggle_header || false);
|
||||||
|
|
||||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
const toggleFilterVisibility = () => {
|
const toggleFilterVisibility = () => {
|
||||||
setIsFilterVisible((prevVisibility) => !prevVisibility);
|
setIsFilterVisible((prevVisibility) => !prevVisibility);
|
||||||
};
|
};
|
||||||
|
|
||||||
const route = all_routes;
|
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 = [
|
const options = [
|
||||||
{ value: "sortByDate", label: "Sort by Date" },
|
{ value: "sortByDate", label: "Sort by Date" },
|
||||||
{ value: "140923", label: "14 09 23" },
|
{ value: "140923", label: "14 09 23" },
|
||||||
@ -73,7 +150,10 @@ const ProductList = () => {
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<span className="productimgname">
|
<span className="productimgname">
|
||||||
<Link to="/profile" className="product-img stock-img">
|
<Link to="/profile" className="product-img stock-img">
|
||||||
<ImageWithBasePath alt="" src={record.productImage} />
|
<ImageWithBasePath
|
||||||
|
alt={record.name || text || "Product"}
|
||||||
|
src={record.productImage || record.image || record.img}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/profile">{text}</Link>
|
<Link to="/profile">{text}</Link>
|
||||||
</span>
|
</span>
|
||||||
@ -119,7 +199,10 @@ const ProductList = () => {
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<span className="userimgname">
|
<span className="userimgname">
|
||||||
<Link to="/profile" className="product-img">
|
<Link to="/profile" className="product-img">
|
||||||
<ImageWithBasePath alt="" src={record.img} />
|
<ImageWithBasePath
|
||||||
|
alt={record.createdBy || text || "User"}
|
||||||
|
src={record.img || record.avatar || record.userImage}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/profile">{text}</Link>
|
<Link to="/profile">{text}</Link>
|
||||||
</span>
|
</span>
|
||||||
@ -129,20 +212,44 @@ const ProductList = () => {
|
|||||||
{
|
{
|
||||||
title: "Action",
|
title: "Action",
|
||||||
dataIndex: "action",
|
dataIndex: "action",
|
||||||
render: () => (
|
render: (text, record) => (
|
||||||
<td className="action-table-data">
|
<td className="action-table-data">
|
||||||
<div className="edit-delete-action">
|
<div className="edit-delete-action">
|
||||||
<div className="input-block add-lists"></div>
|
<div className="input-block add-lists"></div>
|
||||||
<Link className="me-2 p-2" to={route.productdetails}>
|
<Link className="me-2 p-2" to={route.productdetails}>
|
||||||
<Eye className="feather-view" />
|
<Eye className="feather-view" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link className="me-2 p-2" to={route.editproduct}>
|
<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" />
|
<Edit className="feather-edit" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className="confirm-text p-2"
|
className="confirm-text p-2"
|
||||||
to="#"
|
to="#"
|
||||||
onClick={showConfirmationAlert}
|
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" />
|
<Trash2 className="feather-trash-2" />
|
||||||
</Link>
|
</Link>
|
||||||
@ -154,31 +261,7 @@ const ProductList = () => {
|
|||||||
];
|
];
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
const showConfirmationAlert = () => {
|
// Removed showConfirmationAlert as we handle confirmation inline
|
||||||
MySwal.fire({
|
|
||||||
title: "Are you sure?",
|
|
||||||
text: "You won't be able to revert this!",
|
|
||||||
showCancelButton: true,
|
|
||||||
confirmButtonColor: "#00ff00",
|
|
||||||
confirmButtonText: "Yes, delete it!",
|
|
||||||
cancelButtonColor: "#ff0000",
|
|
||||||
cancelButtonText: "Cancel",
|
|
||||||
}).then((result) => {
|
|
||||||
if (result.isConfirmed) {
|
|
||||||
MySwal.fire({
|
|
||||||
title: "Deleted!",
|
|
||||||
text: "Your file has been deleted.",
|
|
||||||
className: "btn btn-success",
|
|
||||||
confirmButtonText: "OK",
|
|
||||||
customClass: {
|
|
||||||
confirmButton: "btn btn-success",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
MySwal.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTooltip = (props) => (
|
const renderTooltip = (props) => (
|
||||||
<Tooltip id="pdf-tooltip" {...props}>
|
<Tooltip id="pdf-tooltip" {...props}>
|
||||||
@ -292,6 +375,8 @@ const ProductList = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
className="form-control form-control-sm formsearch"
|
className="form-control form-control-sm formsearch"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
<Link to className="btn btn-searchset">
|
<Link to className="btn btn-searchset">
|
||||||
<i data-feather="search" className="feather-search" />
|
<i data-feather="search" className="feather-search" />
|
||||||
@ -406,7 +491,26 @@ const ProductList = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* /Filter */}
|
{/* /Filter */}
|
||||||
<div className="table-responsive">
|
<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} />
|
<Table columns={columns} dataSource={dataSource} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
53
src/services/api.js
Normal file
53
src/services/api.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Create axios instance with base configuration
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
// Add auth token if available
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('API Request:', config.method?.toUpperCase(), config.url);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Request Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log('API Response:', response.status, response.config.url);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Response Error:', error.response?.status, error.response?.data);
|
||||||
|
|
||||||
|
// Handle common error cases
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Unauthorized - redirect to login or refresh token
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
// You can add redirect logic here
|
||||||
|
} else if (error.response?.status === 500) {
|
||||||
|
// Server error
|
||||||
|
console.error('Server Error:', error.response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
128
src/services/productsApi.js
Normal file
128
src/services/productsApi.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import api from './api';
|
||||||
|
|
||||||
|
// Products API endpoints
|
||||||
|
const ENDPOINTS = {
|
||||||
|
PRODUCTS: 'Products',
|
||||||
|
PRODUCT_BY_ID: (id) => `Products/${id}`,
|
||||||
|
CATEGORIES: 'Products/categories',
|
||||||
|
BRANDS: 'Products/brands',
|
||||||
|
SEARCH: 'Products/search',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Products API service
|
||||||
|
export const productsApi = {
|
||||||
|
// Get all products
|
||||||
|
getAllProducts: async (params = {}) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(ENDPOINTS.PRODUCTS, { params });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching products:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get product by ID
|
||||||
|
getProductById: async (id) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(ENDPOINTS.PRODUCT_BY_ID(id));
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching product ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create new product
|
||||||
|
createProduct: async (productData) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post(ENDPOINTS.PRODUCTS, productData);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating product:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update product
|
||||||
|
updateProduct: async (id, productData) => {
|
||||||
|
try {
|
||||||
|
const response = await api.put(ENDPOINTS.PRODUCT_BY_ID(id), productData);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error updating product ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete product
|
||||||
|
deleteProduct: async (id) => {
|
||||||
|
try {
|
||||||
|
const response = await api.delete(ENDPOINTS.PRODUCT_BY_ID(id));
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting product ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Search products
|
||||||
|
searchProducts: async (query, params = {}) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(ENDPOINTS.SEARCH, {
|
||||||
|
params: { q: query, ...params }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching products:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get product categories
|
||||||
|
getCategories: async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(ENDPOINTS.CATEGORIES);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching categories:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get product brands
|
||||||
|
getBrands: async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(ENDPOINTS.BRANDS);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching brands:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
bulkUpdateProducts: async (products) => {
|
||||||
|
try {
|
||||||
|
const response = await api.put(`${ENDPOINTS.PRODUCTS}/bulk`, { products });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error bulk updating products:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
bulkDeleteProducts: async (productIds) => {
|
||||||
|
try {
|
||||||
|
const response = await api.delete(`${ENDPOINTS.PRODUCTS}/bulk`, {
|
||||||
|
data: { ids: productIds }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error bulk deleting products:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default productsApi;
|
||||||
Loading…
x
Reference in New Issue
Block a user