This commit is contained in:
tuanOts 2025-05-25 17:15:22 +07:00
parent 6b6e2bbc8a
commit 9687351177
11 changed files with 1099 additions and 38 deletions

3
.env Normal file
View 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
View 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

View File

@ -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
View 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;

View File

@ -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,

View 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,
});

View File

@ -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;

View 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;

View File

@ -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">
<Table columns={columns} dataSource={dataSource} />
{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
View 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
View 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;