From 9687351177181c9195a01d6a541bfe19d5bd63ef Mon Sep 17 00:00:00 2001 From: tuanOts Date: Sun, 25 May 2025 17:15:22 +0700 Subject: [PATCH] update --- .env | 3 + API_INTEGRATION_README.md | 225 +++++++++++++++++++ src/Router/router.link.jsx | 10 +- src/components/ApiTest.jsx | 102 +++++++++ src/core/img/imagewithbasebath.jsx | 9 +- src/core/redux/actions/productActions.js | 204 +++++++++++++++++ src/core/redux/reducer.jsx | 11 +- src/core/redux/reducers/productReducer.js | 220 ++++++++++++++++++ src/feature-module/inventory/productlist.jsx | 172 +++++++++++--- src/services/api.js | 53 +++++ src/services/productsApi.js | 128 +++++++++++ 11 files changed, 1099 insertions(+), 38 deletions(-) create mode 100644 .env create mode 100644 API_INTEGRATION_README.md create mode 100644 src/components/ApiTest.jsx create mode 100644 src/core/redux/actions/productActions.js create mode 100644 src/core/redux/reducers/productReducer.js create mode 100644 src/services/api.js create mode 100644 src/services/productsApi.js diff --git a/.env b/.env new file mode 100644 index 0000000..a9c8090 --- /dev/null +++ b/.env @@ -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/ diff --git a/API_INTEGRATION_README.md b/API_INTEGRATION_README.md new file mode 100644 index 0000000..f39b34d --- /dev/null +++ b/API_INTEGRATION_README.md @@ -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 ( +
+ {loading &&
Loading...
} + {error &&
Error: {error}
} + {products.map(product => ( +
{product.name}
+ ))} +
+ ); +}; +``` + +## 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 + +``` + +## 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 diff --git a/src/Router/router.link.jsx b/src/Router/router.link.jsx index cb134ad..719863d 100644 --- a/src/Router/router.link.jsx +++ b/src/Router/router.link.jsx @@ -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: , + route: Route, + }, + { + id: 116, path: "/", name: "Root", element: , route: Route, }, { - id: 116, + id: 117, path: "*", name: "NotFound", element: , diff --git a/src/components/ApiTest.jsx b/src/components/ApiTest.jsx new file mode 100644 index 0000000..030a394 --- /dev/null +++ b/src/components/ApiTest.jsx @@ -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 ( +
+
+
+
API Connection Test
+
+
+

API Base URL: {process.env.REACT_APP_API_BASE_URL}

+ + + + {loading && ( +
+
+ Loading... +
+
+ )} + + {error && ( +
+ Error: {error} +
+ )} + + {result && ( +
+ Success! API connection working. +
+ Response Data +
+                  {JSON.stringify(result, null, 2)}
+                
+
+
+ )} +
+
+
+ ); +}; + +export default ApiTest; diff --git a/src/core/img/imagewithbasebath.jsx b/src/core/img/imagewithbasebath.jsx index 1b8de39..976a48e 100644 --- a/src/core/img/imagewithbasebath.jsx +++ b/src/core/img/imagewithbasebath.jsx @@ -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, diff --git a/src/core/redux/actions/productActions.js b/src/core/redux/actions/productActions.js new file mode 100644 index 0000000..8e54ef1 --- /dev/null +++ b/src/core/redux/actions/productActions.js @@ -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, +}); diff --git a/src/core/redux/reducer.jsx b/src/core/redux/reducer.jsx index bddb405..8e4aa8f 100644 --- a/src/core/redux/reducer.jsx +++ b/src/core/redux/reducer.jsx @@ -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; diff --git a/src/core/redux/reducers/productReducer.js b/src/core/redux/reducers/productReducer.js new file mode 100644 index 0000000..61304a1 --- /dev/null +++ b/src/core/redux/reducers/productReducer.js @@ -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; diff --git a/src/feature-module/inventory/productlist.jsx b/src/feature-module/inventory/productlist.jsx index 7ac1a38..e255ad9 100644 --- a/src/feature-module/inventory/productlist.jsx +++ b/src/feature-module/inventory/productlist.jsx @@ -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) => ( - + {text} @@ -119,7 +199,10 @@ const ProductList = () => { render: (text, record) => ( - + {text} @@ -129,20 +212,44 @@ const ProductList = () => { { title: "Action", dataIndex: "action", - render: () => ( + render: (text, record) => (
- + { + // Pre-fetch product details for editing + if (record.id || record.key) { + dispatch(fetchProduct(record.id || record.key)); + } + }} + > { + 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); + } + }); + }} > @@ -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) => ( @@ -292,6 +375,8 @@ const ProductList = () => { type="text" placeholder="Search" className="form-control form-control-sm formsearch" + value={searchTerm} + onChange={handleSearch} /> @@ -406,7 +491,26 @@ const ProductList = () => {
{/* /Filter */}
- + {loading ? ( +
+
+ Loading... +
+

Loading products...

+
+ ) : error ? ( +
+ Error: {error} + +
+ ) : ( +
+ )} diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..9b939d0 --- /dev/null +++ b/src/services/api.js @@ -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; diff --git a/src/services/productsApi.js b/src/services/productsApi.js new file mode 100644 index 0000000..fed26c3 --- /dev/null +++ b/src/services/productsApi.js @@ -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;