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 && (
+
+ )}
+
+ {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;
|