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 WareHouses from "../core/modals/peoples/warehouses";
|
||||
import Coupons from "../feature-module/coupons/coupons";
|
||||
import ApiTest from "../components/ApiTest";
|
||||
import { all_routes } from "./all_routes";
|
||||
export const publicRoutes = [
|
||||
{
|
||||
@ -1382,13 +1383,20 @@ export const publicRoutes = [
|
||||
},
|
||||
{
|
||||
id: 115,
|
||||
path: "/api-test",
|
||||
name: "apitest",
|
||||
element: <ApiTest />,
|
||||
route: Route,
|
||||
},
|
||||
{
|
||||
id: 116,
|
||||
path: "/",
|
||||
name: "Root",
|
||||
element: <Navigate to="/" />,
|
||||
route: Route,
|
||||
},
|
||||
{
|
||||
id: 116,
|
||||
id: 117,
|
||||
path: "*",
|
||||
name: "NotFound",
|
||||
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
|
||||
// Handle both relative and absolute paths
|
||||
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;
|
||||
} else {
|
||||
// Ensure there's always a slash between base_path and src
|
||||
@ -38,7 +43,7 @@ const ImageWithBasePath = (props) => {
|
||||
// Add PropTypes validation
|
||||
ImageWithBasePath.propTypes = {
|
||||
className: PropTypes.string,
|
||||
src: PropTypes.string.isRequired, // Make 'src' required
|
||||
src: PropTypes.string, // Allow src to be optional
|
||||
alt: PropTypes.string,
|
||||
height: 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 productReducer from './reducers/productReducer';
|
||||
|
||||
const rootReducer = (state = initialState, action) => {
|
||||
// Legacy reducer for existing functionality
|
||||
const legacyReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case "Product_list":
|
||||
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;
|
||||
|
||||
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,
|
||||
Trash2,
|
||||
} from "feather-icons-react/build/IconComponents";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import Select from "react-select";
|
||||
@ -24,17 +24,94 @@ 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 = () => {
|
||||
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 data = useSelector((state) => state.toggle_header);
|
||||
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" },
|
||||
@ -73,7 +150,10 @@ const ProductList = () => {
|
||||
render: (text, record) => (
|
||||
<span className="productimgname">
|
||||
<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 to="/profile">{text}</Link>
|
||||
</span>
|
||||
@ -119,7 +199,10 @@ const ProductList = () => {
|
||||
render: (text, record) => (
|
||||
<span className="userimgname">
|
||||
<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 to="/profile">{text}</Link>
|
||||
</span>
|
||||
@ -129,20 +212,44 @@ const ProductList = () => {
|
||||
{
|
||||
title: "Action",
|
||||
dataIndex: "action",
|
||||
render: () => (
|
||||
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}>
|
||||
<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={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" />
|
||||
</Link>
|
||||
@ -154,31 +261,7 @@ const ProductList = () => {
|
||||
];
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
||||
const showConfirmationAlert = () => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
};
|
||||
// Removed showConfirmationAlert as we handle confirmation inline
|
||||
|
||||
const renderTooltip = (props) => (
|
||||
<Tooltip id="pdf-tooltip" {...props}>
|
||||
@ -292,6 +375,8 @@ const ProductList = () => {
|
||||
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" />
|
||||
@ -406,7 +491,26 @@ const ProductList = () => {
|
||||
</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>
|
||||
|
||||
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