diff --git a/public/favicon.png b/public/favicon.png index e35e3e1..36b59bf 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/index.html b/public/index.html index e6cb98a..f63741e 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ - Dreams Pos admin template + APSKEL diff --git a/src/core/modals/inventory/addcategorylist.jsx b/src/core/modals/inventory/addcategorylist.jsx index 9a026b7..9ab84c8 100644 --- a/src/core/modals/inventory/addcategorylist.jsx +++ b/src/core/modals/inventory/addcategorylist.jsx @@ -60,13 +60,13 @@ const AddCategoryList = () => {
-
+

Create Category

@@ -77,7 +77,7 @@ const AddCategoryList = () => { { {
+ >
-
+ +
+ + { + handleSelectChange("outlet_id", selectedOption); + setSelectedOutlet(selectedOption); + }} + /> +
+
+ + { + handleSelectChange("product_id", selectedOption); + setSelectedProduct(selectedOption); + }} + /> +
+
+ + +
-
-
- -
-
-
- - -
-
-
-
- - - -
+
+ +
+
-
@@ -100,167 +267,79 @@ const ManageStockModal = () => { {/* /Add Stock */} {/* Edit Stock */}
-
+
-
+

Edit Stock

+ >
-
-
- + +
+ -
-
-
-
- - -
-
-
-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - - - - -
ProductSKUCategoryQtyAction
-
- - - - - Nike Jordan - -
-
PT002Nike -
- - - - - - + - - -
-
-
- - - - - - -
-
-
-
-
+
+ +
+
+ + +
+
-
diff --git a/src/core/redux/actions/inventoryActions.js b/src/core/redux/actions/inventoryActions.js new file mode 100644 index 0000000..bd6f214 --- /dev/null +++ b/src/core/redux/actions/inventoryActions.js @@ -0,0 +1,143 @@ +import { inventoryApi } from '../../../services/inventoriesApi'; + +// Action Types +export const INVENTORY_ACTIONS = { + // Fetch Inventories + FETCH_INVENTORIES_REQUEST: 'FETCH_INVENTORIES_REQUEST', + FETCH_INVENTORIES_SUCCESS: 'FETCH_INVENTORIES_SUCCESS', + FETCH_INVENTORIES_FAILURE: 'FETCH_INVENTORIES_FAILURE', + + // Fetch Single Inventory + FETCH_INVENTORY_REQUEST: 'FETCH_INVENTORY_REQUEST', + FETCH_INVENTORY_SUCCESS: 'FETCH_INVENTORY_SUCCESS', + FETCH_INVENTORY_FAILURE: 'FETCH_INVENTORY_FAILURE', + + // Create Inventory + CREATE_INVENTORY_REQUEST: 'CREATE_INVENTORY_REQUEST', + CREATE_INVENTORY_SUCCESS: 'CREATE_INVENTORY_SUCCESS', + CREATE_INVENTORY_FAILURE: 'CREATE_INVENTORY_FAILURE', + + // Update Inventory + UPDATE_INVENTORY_REQUEST: 'UPDATE_INVENTORY_REQUEST', + UPDATE_INVENTORY_SUCCESS: 'UPDATE_INVENTORY_SUCCESS', + UPDATE_INVENTORY_FAILURE: 'UPDATE_INVENTORY_FAILURE', + + // Delete Inventory + DELETE_INVENTORY_REQUEST: 'DELETE_INVENTORY_REQUEST', + DELETE_INVENTORY_SUCCESS: 'DELETE_INVENTORY_SUCCESS', + DELETE_INVENTORY_FAILURE: 'DELETE_INVENTORY_FAILURE', + + // Search Inventories + SEARCH_INVENTORIES_REQUEST: 'SEARCH_INVENTORIES_REQUEST', + SEARCH_INVENTORIES_SUCCESS: 'SEARCH_INVENTORIES_SUCCESS', + SEARCH_INVENTORIES_FAILURE: 'SEARCH_INVENTORIES_FAILURE', + + // Clear States + CLEAR_INVENTORY_ERROR: 'CLEAR_INVENTORY_ERROR', + CLEAR_CURRENT_INVENTORY: 'CLEAR_CURRENT_INVENTORY', +}; + +// Action Creators + +export const fetchInventories = (params = {}) => async (dispatch) => { + dispatch({ type: INVENTORY_ACTIONS.FETCH_INVENTORIES_REQUEST }); + + try { + const data = await inventoryApi.getAllInventories(params); + dispatch({ + type: INVENTORY_ACTIONS.FETCH_INVENTORIES_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: INVENTORY_ACTIONS.FETCH_INVENTORIES_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch inventories', + }); + throw error; + } +}; + +export const fetchInventory = (id) => async (dispatch) => { + dispatch({ type: INVENTORY_ACTIONS.FETCH_INVENTORY_REQUEST }); + + try { + const data = await inventoryApi.getInventoryById(id); + dispatch({ + type: INVENTORY_ACTIONS.FETCH_INVENTORY_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: INVENTORY_ACTIONS.FETCH_INVENTORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch inventory', + }); + throw error; + } +}; + +export const createInventory = (inventoryData) => async (dispatch) => { + dispatch({ type: INVENTORY_ACTIONS.CREATE_INVENTORY_REQUEST }); + + try { + const data = await inventoryApi.createInventory(inventoryData); + dispatch({ + type: INVENTORY_ACTIONS.CREATE_INVENTORY_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: INVENTORY_ACTIONS.CREATE_INVENTORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to create inventory', + }); + throw error; + } +}; + +export const updateInventory = (id, inventoryData) => async (dispatch) => { + dispatch({ type: INVENTORY_ACTIONS.UPDATE_INVENTORY_REQUEST }); + + try { + const data = await inventoryApi.updateInventory(id, inventoryData); + dispatch({ + type: INVENTORY_ACTIONS.UPDATE_INVENTORY_SUCCESS, + payload: { id, data }, + }); + return data; + } catch (error) { + dispatch({ + type: INVENTORY_ACTIONS.UPDATE_INVENTORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to update inventory', + }); + throw error; + } +}; + +export const deleteInventory = (id) => async (dispatch) => { + dispatch({ type: INVENTORY_ACTIONS.DELETE_INVENTORY_REQUEST }); + + try { + await inventoryApi.deleteInventory(id); + dispatch({ + type: INVENTORY_ACTIONS.DELETE_INVENTORY_SUCCESS, + payload: id, + }); + return id; + } catch (error) { + dispatch({ + type: INVENTORY_ACTIONS.DELETE_INVENTORY_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to delete inventory', + }); + throw error; + } +}; + +export const clearInventoryError = () => ({ + type: INVENTORY_ACTIONS.CLEAR_INVENTORY_ERROR, +}); + +export const clearCurrentInventory = () => ({ + type: INVENTORY_ACTIONS.CLEAR_CURRENT_INVENTORY, +}); diff --git a/src/core/redux/actions/outletActions.js b/src/core/redux/actions/outletActions.js new file mode 100644 index 0000000..3a6185e --- /dev/null +++ b/src/core/redux/actions/outletActions.js @@ -0,0 +1,138 @@ +import { outletsApi } from '../../../services/outletsApi'; + +// Action Types +export const OUTLET_ACTIONS = { + // Fetch Outlets + FETCH_OUTLETS_REQUEST: 'FETCH_OUTLETS_REQUEST', + FETCH_OUTLETS_SUCCESS: 'FETCH_OUTLETS_SUCCESS', + FETCH_OUTLETS_FAILURE: 'FETCH_OUTLETS_FAILURE', + + // Fetch Single Outlet + FETCH_OUTLET_REQUEST: 'FETCH_OUTLET_REQUEST', + FETCH_OUTLET_SUCCESS: 'FETCH_OUTLET_SUCCESS', + FETCH_OUTLET_FAILURE: 'FETCH_OUTLET_FAILURE', + + // Create Outlet + CREATE_OUTLET_REQUEST: 'CREATE_OUTLET_REQUEST', + CREATE_OUTLET_SUCCESS: 'CREATE_OUTLET_SUCCESS', + CREATE_OUTLET_FAILURE: 'CREATE_OUTLET_FAILURE', + + // Update Outlet + UPDATE_OUTLET_REQUEST: 'UPDATE_OUTLET_REQUEST', + UPDATE_OUTLET_SUCCESS: 'UPDATE_OUTLET_SUCCESS', + UPDATE_OUTLET_FAILURE: 'UPDATE_OUTLET_FAILURE', + + // Delete Outlet + DELETE_OUTLET_REQUEST: 'DELETE_OUTLET_REQUEST', + DELETE_OUTLET_SUCCESS: 'DELETE_OUTLET_SUCCESS', + DELETE_OUTLET_FAILURE: 'DELETE_OUTLET_FAILURE', + + // Clear States + CLEAR_OUTLET_ERROR: 'CLEAR_OUTLET_ERROR', + CLEAR_CURRENT_OUTLET: 'CLEAR_CURRENT_OUTLET', +}; + +// Action Creators + +export const fetchOutlets = (params = {}) => async (dispatch) => { + dispatch({ type: OUTLET_ACTIONS.FETCH_OUTLETS_REQUEST }); + + try { + const data = await outletsApi.getAllOutlets(params); + dispatch({ + type: OUTLET_ACTIONS.FETCH_OUTLETS_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: OUTLET_ACTIONS.FETCH_OUTLETS_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch outlets', + }); + throw error; + } +}; + +export const fetchOutlet = (id) => async (dispatch) => { + dispatch({ type: OUTLET_ACTIONS.FETCH_OUTLET_REQUEST }); + + try { + const data = await outletsApi.getOutletById(id); + dispatch({ + type: OUTLET_ACTIONS.FETCH_OUTLET_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: OUTLET_ACTIONS.FETCH_OUTLET_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to fetch outlet', + }); + throw error; + } +}; + +export const createOutlet = (outletData) => async (dispatch) => { + dispatch({ type: OUTLET_ACTIONS.CREATE_OUTLET_REQUEST }); + + try { + const data = await outletsApi.createOutlet(outletData); + dispatch({ + type: OUTLET_ACTIONS.CREATE_OUTLET_SUCCESS, + payload: data, + }); + return data; + } catch (error) { + dispatch({ + type: OUTLET_ACTIONS.CREATE_OUTLET_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to create outlet', + }); + throw error; + } +}; + +export const updateOutlet = (id, outletData) => async (dispatch) => { + dispatch({ type: OUTLET_ACTIONS.UPDATE_OUTLET_REQUEST }); + + try { + const data = await outletsApi.updateOutlet(id, outletData); + dispatch({ + type: OUTLET_ACTIONS.UPDATE_OUTLET_SUCCESS, + payload: { id, data }, + }); + return data; + } catch (error) { + dispatch({ + type: OUTLET_ACTIONS.UPDATE_OUTLET_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to update outlet', + }); + throw error; + } +}; + +export const deleteOutlet = (id) => async (dispatch) => { + dispatch({ type: OUTLET_ACTIONS.DELETE_OUTLET_REQUEST }); + + try { + await outletsApi.deleteOutlet(id); + dispatch({ + type: OUTLET_ACTIONS.DELETE_OUTLET_SUCCESS, + payload: id, + }); + return id; + } catch (error) { + dispatch({ + type: OUTLET_ACTIONS.DELETE_OUTLET_FAILURE, + payload: error.response?.data?.message || error.message || 'Failed to delete outlet', + }); + throw error; + } +}; + +export const clearOutletError = () => ({ + type: OUTLET_ACTIONS.CLEAR_OUTLET_ERROR, +}); + +export const clearCurrentOutlet = () => ({ + type: OUTLET_ACTIONS.CLEAR_CURRENT_OUTLET, +}); diff --git a/src/core/redux/reducer.jsx b/src/core/redux/reducer.jsx index e3f10a9..9633017 100644 --- a/src/core/redux/reducer.jsx +++ b/src/core/redux/reducer.jsx @@ -6,6 +6,8 @@ import categoryReducer from './reducers/categoryReducer'; import orderReducer from './reducers/orderReducer'; import paymentMethodReducer from './reducers/paymentMethodReducer'; import organizationReducer from './reducers/organizationReducer'; +import inventoryReducer from './reducers/inventoryReducer'; +import outletReducer from './reducers/outletReducer'; // Legacy reducer for existing functionality const legacyReducer = (state = initialState, action) => { @@ -82,7 +84,9 @@ const rootReducer = combineReducers({ categories: categoryReducer, orders: orderReducer, paymentMethods: paymentMethodReducer, - organizations: organizationReducer + organizations: organizationReducer, + inventories: inventoryReducer, + outlets: outletReducer }); export default rootReducer; diff --git a/src/core/redux/reducers/authReducer.js b/src/core/redux/reducers/authReducer.js index 71d8d05..20cf6da 100644 --- a/src/core/redux/reducers/authReducer.js +++ b/src/core/redux/reducers/authReducer.js @@ -18,17 +18,17 @@ const authSlice = createSlice({ }, loginSuccess: (state, action) => { state.isAuthenticated = true; - state.user = action.payload.user; - state.token = action.payload.token; + state.user = action.payload.data.user; + state.token = action.payload.data.token; state.loading = false; state.error = null; // Store token in localStorage - localStorage.setItem('authToken', action.payload.token); - localStorage.setItem('user', JSON.stringify(action.payload.user)); + localStorage.setItem('authToken', action.payload.data.token); + localStorage.setItem('user', JSON.stringify(action.payload.data.user)); }, loginFailure: (state, action) => { state.loading = false; - state.error = action.payload; + state.error = action.payload.error; state.isAuthenticated = false; state.user = null; state.token = null; diff --git a/src/core/redux/reducers/inventoryReducer.js b/src/core/redux/reducers/inventoryReducer.js new file mode 100644 index 0000000..f41ef42 --- /dev/null +++ b/src/core/redux/reducers/inventoryReducer.js @@ -0,0 +1,189 @@ +import { INVENTORY_ACTIONS } from '../actions/inventoryActions'; + +const initialState = { + // Inventory list + inventories: [], + totalInventories: 0, + currentPage: 1, + totalPages: 1, + pageSize: 10, + hasPrevious: false, + hasNext: false, + + // Current inventory (for edit/view) + currentInventory: null, + + // Search results + searchResults: [], + searchQuery: '', + + // Loading states + loading: false, + inventoryLoading: false, + searchLoading: false, + + // Error states + error: null, + inventoryError: null, + searchError: null, + + // Operation states + creating: false, + updating: false, + deleting: false, +}; + +const inventoryReducer = (state = initialState, action) => { + switch (action.type) { + // Fetch Inventories + case INVENTORY_ACTIONS.FETCH_INVENTORIES_REQUEST: + return { + ...state, + loading: true, + error: null, + }; + + case INVENTORY_ACTIONS.FETCH_INVENTORIES_SUCCESS: { + const { inventory, total_count, page, total_pages, limit } = + action.payload.data; + + return { + ...state, + loading: false, + inventories: inventory, + totalInventories: total_count || inventory.length, + currentPage: page || 1, + totalPages: total_pages || 1, + pageSize: limit || 10, + hasPrevious: false, + hasNext: false, + error: null, + }; + } + + case INVENTORY_ACTIONS.FETCH_INVENTORIES_FAILURE: + return { + ...state, + loading: false, + error: action.payload, + }; + + // Fetch Single Inventory + case INVENTORY_ACTIONS.FETCH_INVENTORY_REQUEST: + return { + ...state, + inventoryLoading: true, + inventoryError: null, + }; + + case INVENTORY_ACTIONS.FETCH_INVENTORY_SUCCESS: + return { + ...state, + inventoryLoading: false, + currentInventory: action.payload.data, + inventoryError: null, + }; + + case INVENTORY_ACTIONS.FETCH_INVENTORY_FAILURE: + return { + ...state, + inventoryLoading: false, + inventoryError: action.payload, + }; + + // Create Inventory + case INVENTORY_ACTIONS.CREATE_INVENTORY_REQUEST: + return { + ...state, + creating: true, + error: null, + }; + + case INVENTORY_ACTIONS.CREATE_INVENTORY_SUCCESS: + return { + ...state, + creating: false, + inventories: [action.payload.data, ...state.inventories], + totalInventories: state.totalInventories + 1, + error: null, + }; + + case INVENTORY_ACTIONS.CREATE_INVENTORY_FAILURE: + return { + ...state, + creating: false, + error: action.payload, + }; + + // Update Inventory + case INVENTORY_ACTIONS.UPDATE_INVENTORY_REQUEST: + return { + ...state, + updating: true, + error: null, + }; + + case INVENTORY_ACTIONS.UPDATE_INVENTORY_SUCCESS: + return { + ...state, + updating: false, + inventories: state.inventories.map(inv => + inv.id === action.payload.data.id ? action.payload.data : inv + ), + currentInventory: action.payload.data, + error: null, + }; + + case INVENTORY_ACTIONS.UPDATE_INVENTORY_FAILURE: + return { + ...state, + updating: false, + error: action.payload, + }; + + // Delete Inventory + case INVENTORY_ACTIONS.DELETE_INVENTORY_REQUEST: + return { + ...state, + deleting: true, + error: null, + }; + + case INVENTORY_ACTIONS.DELETE_INVENTORY_SUCCESS: + return { + ...state, + deleting: false, + inventories: state.inventories.filter(inv => inv.id !== action.payload), + totalInventories: state.totalInventories - 1, + error: null, + }; + + case INVENTORY_ACTIONS.DELETE_INVENTORY_FAILURE: + return { + ...state, + deleting: false, + error: action.payload, + }; + + // Clear states + case INVENTORY_ACTIONS.CLEAR_INVENTORY_ERROR: + return { + ...state, + error: null, + inventoryError: null, + searchError: null, + }; + + case INVENTORY_ACTIONS.CLEAR_CURRENT_INVENTORY: + return { + ...state, + currentInventory: null, + inventoryError: null, + }; + + default: + return state; + } +}; + +export default inventoryReducer; diff --git a/src/core/redux/reducers/outletReducer.js b/src/core/redux/reducers/outletReducer.js new file mode 100644 index 0000000..fec3365 --- /dev/null +++ b/src/core/redux/reducers/outletReducer.js @@ -0,0 +1,212 @@ +import { OUTLET_ACTIONS } from '../actions/outletActions'; + +const initialState = { + // Outlets list + outlets: [], + totalOutlets: 0, + currentPage: 1, + totalPages: 1, + pageSize: 10, + hasPrevious: false, + hasNext: false, + + // Current outlet (for edit/view) + currentOutlet: null, + + // Search results + searchResults: [], + searchQuery: '', + + // Loading states + loading: false, + outletLoading: false, + searchLoading: false, + + // Error states + error: null, + outletError: null, + searchError: null, + + // Operation states + creating: false, + updating: false, + deleting: false, +}; + +const outletReducer = (state = initialState, action) => { + switch (action.type) { + // Fetch Outlets + case OUTLET_ACTIONS.FETCH_OUTLETS_REQUEST: + return { + ...state, + loading: true, + error: null, + }; + + case OUTLET_ACTIONS.FETCH_OUTLETS_SUCCESS: { + const { outlets, total_count, page, total_pages, limit } = action.payload.data; + + return { + ...state, + loading: false, + outlets: outlets, + totalOutlets: total_count || outlets.length, + currentPage: page || 1, + totalPages: total_pages || 1, + pageSize: limit || 10, + hasPrevious: false, + hasNext: false, + error: null, + }; + } + + case OUTLET_ACTIONS.FETCH_OUTLETS_FAILURE: + return { + ...state, + loading: false, + error: action.payload, + }; + + // Fetch Single Outlet + case OUTLET_ACTIONS.FETCH_OUTLET_REQUEST: + return { + ...state, + outletLoading: true, + outletError: null, + }; + + case OUTLET_ACTIONS.FETCH_OUTLET_SUCCESS: + return { + ...state, + outletLoading: false, + currentOutlet: action.payload.data, + outletError: null, + }; + + case OUTLET_ACTIONS.FETCH_OUTLET_FAILURE: + return { + ...state, + outletLoading: false, + outletError: action.payload, + }; + + // Create Outlet + case OUTLET_ACTIONS.CREATE_OUTLET_REQUEST: + return { + ...state, + creating: true, + error: null, + }; + + case OUTLET_ACTIONS.CREATE_OUTLET_SUCCESS: + return { + ...state, + creating: false, + outlets: [action.payload.data, ...state.outlets], + totalOutlets: state.totalOutlets + 1, + error: null, + }; + + case OUTLET_ACTIONS.CREATE_OUTLET_FAILURE: + return { + ...state, + creating: false, + error: action.payload, + }; + + // Update Outlet + case OUTLET_ACTIONS.UPDATE_OUTLET_REQUEST: + return { + ...state, + updating: true, + error: null, + }; + + case OUTLET_ACTIONS.UPDATE_OUTLET_SUCCESS: + return { + ...state, + updating: false, + outlets: state.outlets.map((outlet) => + outlet.id === action.payload.data.id ? action.payload.data : outlet + ), + currentOutlet: action.payload.data, + error: null, + }; + + case OUTLET_ACTIONS.UPDATE_OUTLET_FAILURE: + return { + ...state, + updating: false, + error: action.payload, + }; + + // Delete Outlet + case OUTLET_ACTIONS.DELETE_OUTLET_REQUEST: + return { + ...state, + deleting: true, + error: null, + }; + + case OUTLET_ACTIONS.DELETE_OUTLET_SUCCESS: + return { + ...state, + deleting: false, + outlets: state.outlets.filter((outlet) => outlet.id !== action.payload), + totalOutlets: state.totalOutlets - 1, + error: null, + }; + + case OUTLET_ACTIONS.DELETE_OUTLET_FAILURE: + return { + ...state, + deleting: false, + error: action.payload, + }; + + // Search Outlets + case OUTLET_ACTIONS.SEARCH_OUTLETS_REQUEST: + return { + ...state, + searchLoading: true, + searchError: null, + }; + + case OUTLET_ACTIONS.SEARCH_OUTLETS_SUCCESS: + return { + ...state, + searchLoading: false, + searchResults: action.payload.data || action.payload, + searchQuery: action.payload.query || '', + searchError: null, + }; + + case OUTLET_ACTIONS.SEARCH_OUTLETS_FAILURE: + return { + ...state, + searchLoading: false, + searchError: action.payload, + }; + + // Clear States + case OUTLET_ACTIONS.CLEAR_OUTLET_ERROR: + return { + ...state, + error: null, + outletError: null, + searchError: null, + }; + + case OUTLET_ACTIONS.CLEAR_CURRENT_OUTLET: + return { + ...state, + currentOutlet: null, + outletError: null, + }; + + default: + return state; + } +}; + +export default outletReducer; diff --git a/src/feature-module/inventory/addproduct.jsx b/src/feature-module/inventory/addproduct.jsx index be9b0b9..65fb835 100644 --- a/src/feature-module/inventory/addproduct.jsx +++ b/src/feature-module/inventory/addproduct.jsx @@ -20,13 +20,12 @@ import { Link, useNavigate } from "react-router-dom"; import Select from "react-select"; import Swal from "sweetalert2"; import ImageWithBasePath from "../../core/img/imagewithbasebath"; -import AddBrand from "../../core/modals/addbrand"; import AddCategory from "../../core/modals/inventory/addcategory"; -import Addunits from "../../core/modals/inventory/addunits"; import { setToogleHeader } from "../../core/redux/action"; import { createProduct } from "../../core/redux/actions/productActions"; import { all_routes } from "../../Router/all_routes"; import { categoriesApi } from "../../services/categoriesApi"; +import { filesApi } from "../../services/filesApi"; const AddProduct = () => { const route = all_routes; @@ -48,14 +47,19 @@ const AddProduct = () => { const [formData, setFormData] = useState({ name: "", sku: "", + barcode: "", description: "", price: 0, cost: 0, category_id: "", + printer_type: "", + image_url: "", }); + const [tempImage, setTempImage] = useState(null); + const [uploading, setUploading] = useState(false); const [variants, setVariants] = useState([ - { name: "", price_modifier: 0, cost: 0 }, + { name: "", price_modifier: '', cost: '' }, ]); const handleInputChange = (e) => { @@ -65,6 +69,38 @@ const AddProduct = () => { }); }; + const handleFileChange = async (e) => { + setTempImage(e.target.files[0]); + }; + + useEffect(() => { + const loadImage = async () => { + setUploading(true); + + try { + const uploads = new FormData(); + + uploads.append("file", tempImage); + uploads.append("file_type", "image"); + uploads.append("description", "Product image"); + + const response = await filesApi.uploadFile(uploads); + + setFormData({ + ...formData, + image_url: response?.data?.file_url, + }); + + setUploading(false); + } catch (error) { + console.error("Error fetching image:", error); + setUploading(false); + } + }; + + loadImage(); + }, [tempImage]); + useEffect(() => { const loadCategories = async () => { try { @@ -90,32 +126,11 @@ const AddProduct = () => { { value: "rasmussen", label: "Rasmussen" }, { value: "fredJohn", label: "Fred John" }, ]; - const warehouse = [ - { value: "choose", label: "Choose" }, - { value: "legendary", label: "Legendary" }, + const printerTypes = [ + { value: "Kitchen", label: "Kitchen" }, { value: "determined", label: "Determined" }, { value: "sincere", label: "Sincere" }, ]; - const subcategory = [ - { value: "choose", label: "Choose" }, - { value: "lenovo", label: "Lenovo" }, - { value: "electronics", label: "Electronics" }, - ]; - const brand = [ - { value: "choose", label: "Choose" }, - { value: "nike", label: "Nike" }, - { value: "bolt", label: "Bolt" }, - ]; - const unit = [ - { value: "choose", label: "Choose" }, - { value: "kg", label: "Kg" }, - { value: "pc", label: "Pc" }, - ]; - const sellingtype = [ - { value: "choose", label: "Choose" }, - { value: "transactionalSelling", label: "Transactional selling" }, - { value: "solutionSelling", label: "Solution selling" }, - ]; const barcodesymbol = [ { value: "choose", label: "Choose" }, { value: "code34", label: "Code34" }, @@ -136,15 +151,9 @@ const AddProduct = () => { { value: "percentage", label: "Percentage" }, { value: "cash", label: "Cash" }, ]; - const [isImageVisible, setIsImageVisible] = useState(true); - - const handleRemoveProduct = () => { - setIsImageVisible(false); - }; - const [isImageVisible1, setIsImageVisible1] = useState(true); const handleRemoveProduct1 = () => { - setIsImageVisible1(false); + setFormData({ ...formData, image_url: "" }); }; // Handle form submission @@ -229,11 +238,17 @@ const AddProduct = () => { Swal.fire({ icon: "error", title: "Error!", - text: error?.response?.data?.errors[0].cause || "Failed to create product. Please try again.", + text: + error?.response?.data?.errors[0].cause || + "Failed to create product. Please try again.", }); } }; + // const handleUploadImage = () => { + + // } + // Handle select changes const handleSelectChange = (field, selectedOption) => { setFormData({ @@ -350,29 +365,6 @@ const AddProduct = () => { data-bs-parent="#accordionExample" >
-
-
-
- - -
-
-
-
@@ -386,12 +378,58 @@ const AddProduct = () => { />
+
+
+ + Store + + option.value === formData.printer_type + ) || null + } + onChange={(selectedOption) => + handleSelectChange( + "printer_type", + selectedOption + ) + } />
@@ -429,164 +467,38 @@ const AddProduct = () => {
- +
+ + + + Add New + +
- option.value === formData.category_id - ) || null - } - onChange={(selectedOption) => - handleSelectChange( - "category_id", - selectedOption - ) - } - /> -
-
-
-
- - -
-
-
-
-
- - - - Add New - -
- -
-
-
- -
- - -
-
- {/*
-
- - - - Generate Code - -
-
*/} -
{/* Editor */}
@@ -935,17 +847,36 @@ const AddProduct = () => {
- +
- -

Add Images

+ {uploading ? ( + <> + +

Uploading...

+ + ) : ( + <> + +

+ {tempImage ? "Edit Image" : "Add Image"} +

+ + )}
- {isImageVisible1 && ( + {formData.image_url && (
@@ -956,20 +887,6 @@ const AddProduct = () => {
)} - {isImageVisible && ( -
- - - - -
- )}
@@ -1131,9 +1048,7 @@ const AddProduct = () => { {/* /add */}
- -
); }; diff --git a/src/feature-module/inventory/categorylist.jsx b/src/feature-module/inventory/categorylist.jsx index b5a1b17..21212d5 100644 --- a/src/feature-module/inventory/categorylist.jsx +++ b/src/feature-module/inventory/categorylist.jsx @@ -235,24 +235,7 @@ const CategoryList = () => { }, ]; 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) { - // handleDeleteCategory(); - // } else { - // MySwal.close(); - // } - // }); - // }; + return (
diff --git a/src/feature-module/inventory/editproduct.jsx b/src/feature-module/inventory/editproduct.jsx index ac896aa..f6c7652 100644 --- a/src/feature-module/inventory/editproduct.jsx +++ b/src/feature-module/inventory/editproduct.jsx @@ -20,9 +20,7 @@ import { Link, useNavigate, useParams } from "react-router-dom"; import Select from "react-select"; import Swal from "sweetalert2"; import ImageWithBasePath from "../../core/img/imagewithbasebath"; -import AddBrand from "../../core/modals/addbrand"; import AddCategory from "../../core/modals/inventory/addcategory"; -import Addunits from "../../core/modals/inventory/addunits"; import { setToogleHeader } from "../../core/redux/action"; import { fetchProduct, @@ -30,6 +28,7 @@ import { } from "../../core/redux/actions/productActions"; import { all_routes } from "../../Router/all_routes"; import categoriesApi from "../../services/categoriesApi"; +import { filesApi } from "../../services/filesApi"; const EditProduct = () => { const route = all_routes; @@ -52,16 +51,53 @@ const EditProduct = () => { const [formData, setFormData] = useState({ name: "", sku: "", + barcode: "", description: "", price: 0, - category_id: null, cost: 0, + category_id: "", + printer_type: "", + image_url: "", }); + const [tempImage, setTempImage] = useState(null); + const [uploading, setUploading] = useState(false); const [variants, setVariants] = useState([ - { name: "", price_modifier: 0, cost: 0 }, + { name: "", price_modifier: '', cost: '' }, ]); + const handleFileChange = async (e) => { + setTempImage(e.target.files[0]); + }; + + useEffect(() => { + const loadImage = async () => { + setUploading(true); + + try { + const uploads = new FormData(); + + uploads.append("file", tempImage); + uploads.append("file_type", "image"); + uploads.append("description", "Product image"); + + const response = await filesApi.uploadFile(uploads); + + setFormData({ + ...formData, + image_url: response?.data?.file_url, + }); + + setUploading(false); + } catch (error) { + console.error("Error fetching image:", error); + setUploading(false); + } + }; + + loadImage(); + }, [tempImage]); + useEffect(() => { const loadCategories = async () => { try { @@ -98,6 +134,9 @@ const EditProduct = () => { price: currentProduct.price || 0, cost: currentProduct.cost || 0, category_id: currentProduct.category_id, + printer_type: currentProduct.printer_type, + image_url: currentProduct.image_url, + barcode: currentProduct.barcode, }); if (currentProduct.variants) { @@ -134,7 +173,7 @@ const EditProduct = () => { }; const addVariant = () => { - setVariants([...variants, { name: "", price_modifier: 0, cost: 0 }]); + setVariants([...variants, { name: "", price_modifier: '', cost: '' }]); }; const removeVariant = (index) => { @@ -221,7 +260,7 @@ const EditProduct = () => { Swal.fire({ icon: "success", title: "Success!", - text: "Product created successfully!", + text: "Product updated successfully!", showConfirmButton: false, timer: 1500, }); @@ -237,7 +276,7 @@ const EditProduct = () => { Swal.fire({ icon: "error", title: "Error!", - text: error.message || "Failed to create product. Please try again.", + text: error.message || "Failed to update product. Please try again.", }); } }; @@ -248,43 +287,16 @@ const EditProduct = () => { { value: "rasmussen", label: "Rasmussen" }, { value: "fredJohn", label: "Fred John" }, ]; - const warehouse = [ - { value: "choose", label: "Choose" }, - { value: "legendary", label: "Legendary" }, - { value: "determined", label: "Determined" }, - { value: "sincere", label: "Sincere" }, - ]; - const subcategory = [ - { value: "choose", label: "Choose" }, - { value: "lenovo", label: "Lenovo" }, - { value: "electronics", label: "Electronics" }, - ]; - const brand = [ - { value: "choose", label: "Choose" }, - { value: "nike", label: "Nike" }, - { value: "bolt", label: "Bolt" }, - ]; - const unit = [ - { value: "choose", label: "Choose" }, - { value: "kg", label: "Kg" }, - { value: "pc", label: "Pc" }, - { value: "pcs", label: "Pcs" }, - { value: "piece", label: "Piece" }, - { value: "gram", label: "Gram" }, - { value: "liter", label: "Liter" }, - { value: "meter", label: "Meter" }, - ]; - const sellingtype = [ - { value: "choose", label: "Choose" }, - { value: "transactionalSelling", label: "Transactional selling" }, - { value: "solutionSelling", label: "Solution selling" }, - ]; const barcodesymbol = [ - { value: "choose", label: "Choose" }, { value: "code34", label: "Code34" }, { value: "code35", label: "Code35" }, { value: "code36", label: "Code36" }, ]; + const printerTypes = [ + { value: "Kitchen", label: "Kitchen" }, + { value: "determined", label: "Determined" }, + { value: "sincere", label: "Sincere" }, + ]; const taxtype = [ { value: "exclusive", label: "Exclusive" }, { value: "salesTax", label: "Sales Tax" }, @@ -299,15 +311,9 @@ const EditProduct = () => { { value: "percentage", label: "Percentage" }, { value: "cash", label: "Cash" }, ]; - const [isImageVisible, setIsImageVisible] = useState(true); - - const handleRemoveProduct = () => { - setIsImageVisible(false); - }; - const [isImageVisible1, setIsImageVisible1] = useState(true); const handleRemoveProduct1 = () => { - setIsImageVisible1(false); + setFormData({ ...formData, image_url: "" }); }; // Show loading state @@ -413,7 +419,7 @@ const EditProduct = () => {
- {/* /add */} + {/* /edit */}
@@ -448,29 +454,6 @@ const EditProduct = () => { data-bs-parent="#accordionExample" >
-
-
-
- - -
-
-
-
@@ -484,12 +467,58 @@ const EditProduct = () => { />
+
+
+ + Store + + option.value === formData.printer_type + ) || null + } + onChange={(selectedOption) => + handleSelectChange( + "printer_type", + selectedOption + ) + } />
@@ -527,150 +556,38 @@ const EditProduct = () => {
- +
+ + + + Add New + +
- option.value === formData.category_id - ) - : null - } - onChange={(selectedOption) => - handleSelectChange( - "category_id", - selectedOption - ) - } - /> -
-
-
-
- - -
-
-
-
-
- - - - Add New - -
- -
-
-
- -
- - -
-
-
{/* Editor */}
@@ -774,6 +691,7 @@ const EditProduct = () => {
+
{
@@ -1018,17 +936,36 @@ const EditProduct = () => {
- +
- -

Add Images

+ {uploading ? ( + <> + +

Uploading...

+ + ) : ( + <> + +

+ {tempImage ? "Edit Image" : "Add Image"} +

+ + )}
- {isImageVisible1 && ( + {formData.image_url && (
@@ -1039,20 +976,6 @@ const EditProduct = () => {
)} - {isImageVisible && ( -
- - - - -
- )}
@@ -1214,9 +1137,7 @@ const EditProduct = () => { {/* /add */}
- -
); }; diff --git a/src/feature-module/inventory/outletlist.jsx b/src/feature-module/inventory/outletlist.jsx new file mode 100644 index 0000000..a0b4d50 --- /dev/null +++ b/src/feature-module/inventory/outletlist.jsx @@ -0,0 +1,388 @@ +import { Select, Tag } from "antd"; +import { + ChevronUp, + PlusCircle, + RotateCcw, + Trash2, +} from "feather-icons-react/build/IconComponents"; +import { useEffect, useState } from "react"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import { useDispatch, useSelector } from "react-redux"; +import { Link } from "react-router-dom"; +import Swal from "sweetalert2"; +import withReactContent from "sweetalert2-react-content"; +import CustomPagination from "../../components/CustomPagination"; +import ImageWithBasePath from "../../core/img/imagewithbasebath"; +import AddCategoryList from "../../core/modals/inventory/addcategorylist"; +import EditCategoryList from "../../core/modals/inventory/editcategorylist"; +import Table from "../../core/pagination/datatable"; +import { setToogleHeader } from "../../core/redux/action"; +import { + deleteCategory, + fetchCategories, + fetchCategory, +} from "../../core/redux/actions/categoryActions"; +import { formatDate } from "../../utils/date"; + +const OutletList = () => { + const { + categories: apiCategories, + loading, + error, + totalCategories, + totalPages, + pageSize: reduxPageSize, + currentPage: reduxCurrentPage, + } = useSelector((state) => state.categories); + + const dispatch = useDispatch(); + const data = useSelector((state) => state.toggle_header); + const dataSource = apiCategories?.length > 0 ? apiCategories : []; + + const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1); + const [pageSize, setPageSize] = useState(reduxPageSize || 10); + const [searchTerm, setSearchTerm] = useState(""); + + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + + useEffect(() => { + const loadCategories = async () => { + try { + const searchParams = { + page: currentPage, + limit: pageSize, + search: debouncedSearchTerm || "", + }; + + // Remove empty parameters + const cleanParams = Object.fromEntries( + Object.entries(searchParams).filter(([, value]) => value !== "") + ); + + await dispatch(fetchCategories(cleanParams)); + } catch (error) { + console.error("Failed to load categories", error); + } + }; + + loadCategories(); + }, [dispatch, currentPage, pageSize, debouncedSearchTerm]); + + // Debounce search term + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + }, 500); // 500ms delay + + return () => clearTimeout(timer); + }, [searchTerm]); + + // Handle pagination + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + // Handle page size change + const handlePageSizeChange = (newPageSize) => { + setPageSize(newPageSize); + setCurrentPage(1); // Reset to first page when changing page size + }; + + const handleSearch = (e) => { + const value = e.target.value; + setSearchTerm(value); + // Reset to first page when searching + setCurrentPage(1); + }; + + // Calculate pagination info + const totalRecords = totalCategories || dataSource.length; + const calculatedTotalPages = Math.ceil(totalRecords / pageSize); + const actualTotalPages = totalPages || calculatedTotalPages; + + const handleDeleteCategory = async (categoryId) => { + try { + await dispatch(deleteCategory(categoryId)); + // Show success message + MySwal.fire({ + title: "Deleted!", + text: "Category has been deleted successfully.", + icon: "success", + className: "btn btn-success", + customClass: { + confirmButton: "btn btn-success", + }, + }); + } catch (error) { + console.error("Failed to delete category:", error); + MySwal.fire({ + title: "Error!", + text: "Failed to delete category. Please try again.", + icon: "error", + className: "btn btn-danger", + customClass: { + confirmButton: "btn btn-danger", + }, + }); + } + }; + + const renderTooltip = (props) => ( + + Pdf + + ); + const renderExcelTooltip = (props) => ( + + Excel + + ); + const renderPrinterTooltip = (props) => ( + + Printer + + ); + const renderRefreshTooltip = (props) => ( + + Refresh + + ); + const renderCollapseTooltip = (props) => ( + + Collapse + + ); + + const dateOptions = [ + { label: "Sort By: Last 7 Days", value: "last7days" }, + { label: "Sort By: Last Month", value: "lastmonth" }, + { label: "Sort By: Ascending", value: "ascending" }, + { label: "Sort By: Descending", value: "descending" }, + ]; + + const columns = [ + { + title: "Category", + dataIndex: "category", + render: (_, record) => { + return {record.name}; + }, + sorter: (a, b) => a.category.length - b.category.length, + }, + { + title: "Category Slug", + dataIndex: "categoryslug", + render: (_, record) => { + return {record?.name?.toLowerCase()}; + }, + sorter: (a, b) => a.categoryslug.length - b.categoryslug.length, + }, + { + title: "Created On", + dataIndex: "createdon", + render: (_, record) => { + return {formatDate(record.created_at)}; + }, + sorter: (a, b) => a.createdon.length - b.createdon.length, + }, + { + title: "Status", + dataIndex: "status", + render: () => active, + sorter: (a, b) => a.status.length - b.status.length, + }, + { + title: "Actions", + dataIndex: "actions", + key: "actions", + render: (_, record) => ( + +
+ dispatch(fetchCategory(record.id))} + > + + + { + 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) { + handleDeleteCategory(record.id || record.key); + } + }); + }} + > + + +
+ + ), + }, + ]; + const MySwal = withReactContent(Swal); + + return ( +
+
+
+
+
+
+

Category

+
Manage your categories
+
+
+
    +
  • + + + + + +
  • +
  • + + + + + +
  • +
  • + + + + + +
  • +
  • + + + + + +
  • +
  • + + { + dispatch(setToogleHeader(!data)); + }} + > + + + +
  • +
+
+ + + Add New Category + +
+
+ {/* /product list */} +
+
+
+
+
+ + + + +
+
+ + {
-
diff --git a/src/feature-module/sales/saleslist.jsx b/src/feature-module/sales/saleslist.jsx index 753584e..f58ec60 100644 --- a/src/feature-module/sales/saleslist.jsx +++ b/src/feature-module/sales/saleslist.jsx @@ -84,6 +84,7 @@ const SalesList = () => { useEffect(() => { const timer = setTimeout(() => { setDebouncedSearchTerm(searchTerm); + handleSetParams("page", 1); }, 500); // 500ms delay return () => clearTimeout(timer); @@ -93,8 +94,6 @@ const SalesList = () => { const handleSearch = (e) => { const value = e.target.value; setSearchTerm(value); - // Reset to first page when searching - handleSetParams("page", 1); }; // Handle pagination @@ -824,19 +823,27 @@ const SalesList = () => { Order Tax - {formatRupiah(selectedOrder?.tax_amount)} + + {formatRupiah(selectedOrder?.tax_amount)} + Discount - {formatRupiah(selectedOrder?.discount_amount)} + + {formatRupiah(selectedOrder?.discount_amount)} + Grand Total - {formatRupiah(selectedOrder?.total_amount)} + + {formatRupiah(selectedOrder?.total_amount)} + Sub Total - {formatRupiah(selectedOrder?.subtotal)} + + {formatRupiah(selectedOrder?.subtotal)} + diff --git a/src/feature-module/stock/managestock.jsx b/src/feature-module/stock/managestock.jsx index 6f8e092..46fe0f3 100644 --- a/src/feature-module/stock/managestock.jsx +++ b/src/feature-module/stock/managestock.jsx @@ -1,115 +1,181 @@ -import React, { useState } from "react"; -import Breadcrumbs from "../../core/breadcrumbs"; -import { Filter, Sliders } from "react-feather"; -import ImageWithBasePath from "../../core/img/imagewithbasebath"; -import Select from "react-select"; -import { Link } from "react-router-dom"; -import DatePicker from "react-datepicker"; +import { Tag } from "antd"; +import { useEffect, useState } from "react"; import "react-datepicker/dist/react-datepicker.css"; -import { Archive, Box, Calendar, User } from "react-feather"; -import ManageStockModal from "../../core/modals/stocks/managestockModal"; import { Edit, Trash2 } from "react-feather"; +import { useDispatch, useSelector } from "react-redux"; +import { Link } from "react-router-dom"; import Swal from "sweetalert2"; import withReactContent from "sweetalert2-react-content"; +import CustomPagination from "../../components/CustomPagination"; +import Breadcrumbs from "../../core/breadcrumbs"; +import ImageWithBasePath from "../../core/img/imagewithbasebath"; +import ManageStockModal from "../../core/modals/stocks/managestockModal"; import Table from "../../core/pagination/datatable"; -import { useSelector } from "react-redux"; +import { + deleteInventory, + fetchInventories, + INVENTORY_ACTIONS, +} from "../../core/redux/actions/inventoryActions"; +import { formatDate } from "../../utils/date"; const Managestock = () => { - const data = useSelector((state) => state.managestockdata); + const { + inventories: apiInventories, + loading, + error, + totalInventories, + totalPages, + pageSize: reduxPageSize, + currentPage: reduxCurrentPage, + } = useSelector((state) => state.inventories); - const [isFilterVisible, setIsFilterVisible] = useState(false); - const [selectedDate, setSelectedDate] = useState(null); + const dispatch = useDispatch(); + const dataSource = apiInventories?.length > 0 ? apiInventories : []; - const toggleFilterVisibility = () => { - setIsFilterVisible((prevVisibility) => !prevVisibility); - }; - const handleDateChange = (date) => { - setSelectedDate(date); + const [params, setParams] = useState({ + page: reduxCurrentPage || 1, + limit: reduxPageSize || 10, + }); + const [searchTerm, setSearchTerm] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + + useEffect(() => { + const loadInventories = async () => { + try { + const receivedParams = { + page: params.page, + limit: params.limit, + search: debouncedSearchTerm || "", + }; + + // Remove empty parameters + const cleanParams = Object.fromEntries( + Object.entries(receivedParams).filter(([, value]) => value !== "") + ); + + await dispatch(fetchInventories(cleanParams)); + } catch (error) { + console.error("Failed to fetch orders", error); + } + }; + + loadInventories(); + }, [dispatch, params, debouncedSearchTerm]); + + // Debounce search term + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + handleSetParams("page", 1); + }, 500); // 500ms delay + + return () => clearTimeout(timer); + }, [searchTerm]); + + const handleSetParams = (key, value) => { + setParams({ + ...params, + [key]: value, + }); }; - const options = [ - { value: "sortByDate", label: "Sort by Date" }, - { value: "140923", label: "14 09 23" }, - { value: "110923", label: "11 09 23" }, - ]; + const handleSearch = (e) => { + const value = e.target.value; + setSearchTerm(value); + }; - const warehouseOptions = [ - { value: "Choose Warehouse", label: "Choose Warehouse" }, - { value: "Lobar Handy", label: "Lobar Handy" }, - { value: "Quaint Warehouse", label: "Quaint Warehouse" }, - { value: "Traditional Warehouse", label: "Traditional Warehouse" }, - { value: "Cool Warehouse", label: "Cool Warehouse" }, - ]; + // Handle pagination + const handlePageChange = (page) => { + handleSetParams("page", page); + }; - const productOptions = [ - { value: "Choose Product", label: "Choose Product" }, - { value: "Nike Jordan", label: "Nike Jordan" }, - { value: "Apple Series 5 Watch", label: "Apple Series 5 Watch" }, - { value: "Amazon Echo Dot", label: "Amazon Echo Dot" }, - { value: "Lobar Handy", label: "Lobar Handy" }, - ]; + // Handle page size change + const handlePageSizeChange = (newPageSize) => { + handleSetParams("limit", newPageSize); + handleSetParams("page", 1); + }; - const personOptions = [ - { value: "Choose Person", label: "Choose Person" }, - { value: "Steven", label: "Steven" }, - { value: "Gravely", label: "Gravely" }, - ]; + const handleDelete = async (id) => { + try { + await dispatch(deleteInventory(id)); + + MySwal.fire({ + title: "Deleted!", + text: "Your stock has been deleted.", + className: "btn btn-success", + confirmButtonText: "OK", + customClass: { + confirmButton: "btn btn-success", + }, + }); + } catch (error) { + console.error("Failed to delete inventory", error); + } + }; + + // Calculate pagination info + const totalRecords = totalInventories || dataSource.length; + const calculatedTotalPages = Math.ceil(totalRecords / params.limit); + const actualTotalPages = totalPages || calculatedTotalPages; const columns = [ { - title: "Warehouse", - dataIndex: "Warehouse", + title: "Outlet", + dataIndex: "outlet", + render: (_, record) => {record.outlet || "-"}, sorter: (a, b) => a.Warehouse.length - b.Warehouse.length, }, - { - title: "Shop", - dataIndex: "Shop", - sorter: (a, b) => a.Shop.length - b.Shop.length, - }, { title: "Product", dataIndex: "Product", render: (text, record) => ( - + - {record.Product.Name} + {record.product_name || ""} ), sorter: (a, b) => a.Product.Name.length - b.Product.Name.length, }, { - title: "Date", - dataIndex: "Date", + title: "Qty", + dataIndex: "qty", + render: (_, record) => {record.quantity || 0}, sorter: (a, b) => a.Email.length - b.Email.length, }, { - title: "Person", - dataIndex: "Person", - render: (text, record) => ( - - - - - {record.Person.Name} - + title: "Status", + dataIndex: "status", + render: (_, record) => ( + + {record.is_low_stock ? "Low Stock" : "Normal Stock"} + ), sorter: (a, b) => a.Person.Name.length - b.Person.Name.length, }, { - title: "Quantity", - dataIndex: "Quantity", + title: "Record Level", + dataIndex: "recordlevel", + render: (_, record) => {record.record_level || "-"}, + sorter: (a, b) => a.Quantity.length - b.Quantity.length, + }, + { + title: "Updated Date", + dataIndex: "updateddate", + render: (_, record) => ( + {formatDate(record.updated_at) || "-"} + ), sorter: (a, b) => a.Quantity.length - b.Quantity.length, }, { title: "Action", dataIndex: "action", - render: () => ( + render: (_, record) => (
@@ -119,6 +185,12 @@ const Managestock = () => { to="#" data-bs-toggle="modal" data-bs-target="#edit-units" + onClick={() => + dispatch({ + type: INVENTORY_ACTIONS.FETCH_INVENTORY_SUCCESS, + payload: { data: record }, + }) + } > @@ -126,7 +198,7 @@ const Managestock = () => { showConfirmationAlert(record.id)} > @@ -139,7 +211,7 @@ const Managestock = () => { const MySwal = withReactContent(Swal); - const showConfirmationAlert = () => { + const showConfirmationAlert = (id) => { MySwal.fire({ title: "Are you sure?", text: "You won't be able to revert this!", @@ -150,20 +222,13 @@ const Managestock = () => { 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", - }, - }); + handleDelete(id); } else { MySwal.close(); } }); }; + return (
@@ -182,117 +247,58 @@ const Managestock = () => { type="text" placeholder="Search" className="form-control form-control-sm formsearch" + value={searchTerm} + onChange={handleSearch} />
-
- - - - - - -
-
- - - -
-
-
-
- - -
-
- -
-
-
- {/* /Filter */}
- record.id} - // pagination={true} - /> + {loading ? ( +
+
+ Loading... +
+

Loading products...

+
+ ) : error ? ( +
+ Error: {error} + +
+ ) : ( + <> +
record.id} + /> + + + + )} diff --git a/src/feature-module/stock/stockAdjustment.jsx b/src/feature-module/stock/stockAdjustment.jsx index 17ed541..a022839 100644 --- a/src/feature-module/stock/stockAdjustment.jsx +++ b/src/feature-module/stock/stockAdjustment.jsx @@ -1,118 +1,153 @@ -import React, { useState } from "react"; -import Breadcrumbs from "../../core/breadcrumbs"; -import { Filter, Sliders } from "react-feather"; -import ImageWithBasePath from "../../core/img/imagewithbasebath"; -import Select from "react-select"; -import { Link } from "react-router-dom"; -import DatePicker from "react-datepicker"; +import { useEffect, useState } from "react"; import "react-datepicker/dist/react-datepicker.css"; -import { Archive, Box, Calendar, User, Edit, Trash2 } from "react-feather"; +import { Edit, Trash2 } from "react-feather"; +import { useDispatch, useSelector } from "react-redux"; +import { Link } from "react-router-dom"; import Swal from "sweetalert2"; import withReactContent from "sweetalert2-react-content"; -import Table from "../../core/pagination/datatable"; +import CustomPagination from "../../components/CustomPagination"; +import Breadcrumbs from "../../core/breadcrumbs"; +import ImageWithBasePath from "../../core/img/imagewithbasebath"; import StockadjustmentModal from "../../core/modals/stocks/stockadjustmentModal"; -import { useSelector } from "react-redux"; +import Table from "../../core/pagination/datatable"; +import { fetchInventories } from "../../core/redux/actions/inventoryActions"; +import { formatDate } from "../../utils/date"; +import { Tag } from "antd"; const StockAdjustment = () => { - const data = useSelector((state) => state.managestockdata); + const { + inventories: apiInventories, + loading, + error, + totalInventories, + totalPages, + pageSize: reduxPageSize, + currentPage: reduxCurrentPage, + } = useSelector((state) => state.inventories); - const [isFilterVisible, setIsFilterVisible] = useState(false); - const [selectedDate, setSelectedDate] = useState(null); + const dispatch = useDispatch(); + const dataSource = apiInventories?.length > 0 ? apiInventories : []; - const toggleFilterVisibility = () => { - setIsFilterVisible((prevVisibility) => !prevVisibility); + const [params, setParams] = useState({ + page: reduxCurrentPage || 1, + limit: reduxPageSize || 10, + }); + const [searchTerm, setSearchTerm] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); + + useEffect(() => { + const loadInventories = async () => { + try { + const receivedParams = { + page: params.page, + limit: params.limit, + search: debouncedSearchTerm || "", + }; + + // Remove empty parameters + const cleanParams = Object.fromEntries( + Object.entries(receivedParams).filter(([, value]) => value !== "") + ); + + await dispatch(fetchInventories(cleanParams)); + } catch (error) { + console.error("Failed to fetch orders", error); + } + }; + + loadInventories(); + }, [dispatch, params, debouncedSearchTerm]); + + // Debounce search term + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearchTerm(searchTerm); + handleSetParams("page", 1); + }, 500); // 500ms delay + + return () => clearTimeout(timer); + }, [searchTerm]); + + const handleSetParams = (key, value) => { + setParams({ + ...params, + [key]: value, + }); }; - const handleDateChange = (date) => { - setSelectedDate(date); + const handleSearch = (e) => { + const value = e.target.value; + setSearchTerm(value); }; - const options = [ - { value: "sortByDate", label: "Sort by Date" }, - { value: "140923", label: "14 09 23" }, - { value: "110923", label: "11 09 23" }, - ]; - const warehouseOptions = [ - { value: "Choose Warehouse", label: "Choose Warehouse" }, - { value: "Lobar Handy", label: "Lobar Handy" }, - { value: "Quaint Warehouse", label: "Quaint Warehouse" }, - { value: "Traditional Warehouse", label: "Traditional Warehouse" }, - { value: "Cool Warehouse", label: "Cool Warehouse" }, - ]; + // Handle pagination + const handlePageChange = (page) => { + handleSetParams("page", page); + }; - const productOptions = [ - { value: "Choose Product", label: "Choose Product" }, - { value: "Nike Jordan", label: "Nike Jordan" }, - { value: "Apple Series 5 Watch", label: "Apple Series 5 Watch" }, - { value: "Amazon Echo Dot", label: "Amazon Echo Dot" }, - { value: "Lobar Handy", label: "Lobar Handy" }, - ]; + // Handle page size change + const handlePageSizeChange = (newPageSize) => { + handleSetParams("limit", newPageSize); + handleSetParams("page", 1); + }; - const personOptions = [ - { value: "Choose Person", label: "Choose Person" }, - { value: "Steven", label: "Steven" }, - { value: "Gravely", label: "Gravely" }, - ]; + // Calculate pagination info + const totalRecords = totalInventories || dataSource.length; + const calculatedTotalPages = Math.ceil(totalRecords / params.limit); + const actualTotalPages = totalPages || calculatedTotalPages; const columns = [ { - title: "Warehouse", - dataIndex: "Warehouse", + title: "Outlet", + dataIndex: "outlet", + render: (_, record) => {record.outlet || "-"}, sorter: (a, b) => a.Warehouse.length - b.Warehouse.length, }, - { - title: "Shop", - dataIndex: "Shop", - sorter: (a, b) => a.Shop.length - b.Shop.length, - }, { title: "Product", dataIndex: "Product", render: (text, record) => ( - + - {record.Product.Name} + {record.product_name || ""} ), sorter: (a, b) => a.Product.Name.length - b.Product.Name.length, }, { - title: "Date", - dataIndex: "Date", + title: "Qty", + dataIndex: "qty", + render: (_, record) => {record.quantity || 0}, sorter: (a, b) => a.Email.length - b.Email.length, }, { - title: "Person", - dataIndex: "Person", - render: (text, record) => ( - - - - - {record.Person.Name} - + title: "Status", + dataIndex: "status", + render: (_, record) => ( + + {record.is_low_stock ? "Low Stock" : "Normal Stock"} + ), sorter: (a, b) => a.Person.Name.length - b.Person.Name.length, }, { - title: "Notes", - // dataIndex: "Quantity", - render: () => ( - - View Note - + title: "Record Level", + dataIndex: "recordlevel", + render: (_, record) => {record.record_level || "-"}, + sorter: (a, b) => a.Quantity.length - b.Quantity.length, + }, + { + title: "Updated Date", + dataIndex: "updateddate", + render: (_, record) => ( + {formatDate(record.updated_at) || "-"} ), - sorter: (a, b) => a.Notes.length - b.Notes.length, + sorter: (a, b) => a.Quantity.length - b.Quantity.length, }, { @@ -191,115 +226,58 @@ const StockAdjustment = () => { type="text" placeholder="Search" className="form-control form-control-sm formsearch" + name="search" + value={searchTerm} + onChange={handleSearch} /> -
- - - - - - -
-
- - - -
- -
-
- - -
-
- - - - - {/* /Filter */}
-
+ {loading ? ( +
+
+ Loading... +
+

Loading products...

+
+ ) : error ? ( +
+ Error: {error} + +
+ ) : ( + <> +
+ + + + )} diff --git a/src/services/filesApi.js b/src/services/filesApi.js new file mode 100644 index 0000000..c92f975 --- /dev/null +++ b/src/services/filesApi.js @@ -0,0 +1,35 @@ +import api from "./api"; + +// Files API endpoints +const ENDPOINTS = { + FILE_UPLOAD: "files/upload", + FILE_BY_ID: (id) => `files/${id}`, +}; + +// Files API service +export const filesApi = { + // Get all categories + uploadFile: async (fileData) => { + try { + const response = await api.post(ENDPOINTS.FILE_UPLOAD, fileData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + return response.data; + } catch (error) { + console.error("Error fetching categories:", error); + throw error; + } + }, + + getFileById: async (id) => { + try { + const response = await api.get(ENDPOINTS.FILE_BY_ID(id)); + return response.data; + } catch (error) { + console.error(`Error fetching file ${id}:`, error); + throw error; + } + }, +}; diff --git a/src/services/inventoriesApi.js b/src/services/inventoriesApi.js new file mode 100644 index 0000000..03e1ea6 --- /dev/null +++ b/src/services/inventoriesApi.js @@ -0,0 +1,60 @@ +import api from './api'; + +const ENDPOINTS = { + INVENTORIES: 'inventory', + INVENTORY_BY_ID: (id) => `inventory/${id}`, +}; + +export const inventoryApi = { + getAllInventories: async (params = {}) => { + try { + const response = await api.get(ENDPOINTS.INVENTORIES, { params }); + return response.data; + } catch (error) { + console.error('Error fetching inventories:', error); + throw error; + } + }, + + getInventoryById: async (id) => { + try { + const response = await api.get(ENDPOINTS.INVENTORY_BY_ID(id)); + return response.data; + } catch (error) { + console.error(`Error fetching inventory ${id}:`, error); + throw error; + } + }, + + createInventory: async (inventoryData) => { + try { + const response = await api.post(ENDPOINTS.INVENTORIES, inventoryData); + return response.data; + } catch (error) { + console.error('Error creating inventory:', error); + throw error; + } + }, + + updateInventory: async (id, inventoryData) => { + try { + const response = await api.put(ENDPOINTS.INVENTORY_BY_ID(id), inventoryData); + return response.data; + } catch (error) { + console.error(`Error updating inventory ${id}:`, error); + throw error; + } + }, + + deleteInventory: async (id) => { + try { + const response = await api.delete(ENDPOINTS.INVENTORY_BY_ID(id)); + return response.data; + } catch (error) { + console.error(`Error deleting inventory ${id}:`, error); + throw error; + } + }, +}; + +export default inventoryApi; diff --git a/src/services/outletsApi.js b/src/services/outletsApi.js new file mode 100644 index 0000000..c4c0cbf --- /dev/null +++ b/src/services/outletsApi.js @@ -0,0 +1,102 @@ +import api from './api'; + +// Outlets API endpoints +const ENDPOINTS = { + OUTLETS: 'outlets/list', + OUTLET_BY_ID: (id) => `outlets/${id}`, + OUTLET_PRODUCTS: (id) => `outlets/${id}/products`, +}; + +// Outlets API service +export const outletsApi = { + // Get all outlets + getAllOutlets: async (params = {}) => { + try { + const response = await api.get(ENDPOINTS.OUTLETS, { params }); + return response.data; + } catch (error) { + console.error('Error fetching outlets:', error); + throw error; + } + }, + + // Get outlet by ID + getOutletById: async (id) => { + try { + const response = await api.get(ENDPOINTS.OUTLET_BY_ID(id)); + return response.data; + } catch (error) { + console.error(`Error fetching outlet ${id}:`, error); + throw error; + } + }, + + // Create new outlet + createOutlet: async (outletData) => { + try { + const response = await api.post(ENDPOINTS.OUTLETS, outletData); + return response.data; + } catch (error) { + console.error('Error creating outlet:', error); + throw error; + } + }, + + // Update outlet + updateOutlet: async (id, outletData) => { + try { + const response = await api.put(ENDPOINTS.OUTLET_BY_ID(id), outletData); + return response.data; + } catch (error) { + console.error(`Error updating outlet ${id}:`, error); + throw error; + } + }, + + // Delete outlet + deleteOutlet: async (id) => { + try { + const response = await api.delete(ENDPOINTS.OUTLET_BY_ID(id)); + return response.data; + } catch (error) { + console.error(`Error deleting outlet ${id}:`, error); + throw error; + } + }, + + // Get products by outlet + getOutletProducts: async (id, params = {}) => { + try { + const response = await api.get(ENDPOINTS.OUTLET_PRODUCTS(id), { params }); + return response.data; + } catch (error) { + console.error(`Error fetching products for outlet ${id}:`, error); + throw error; + } + }, + + // Bulk operations + bulkUpdateOutlets: async (outlets) => { + try { + const response = await api.put(`${ENDPOINTS.OUTLETS}/bulk`, { outlets }); + return response.data; + } catch (error) { + console.error('Error bulk updating outlets:', error); + throw error; + } + }, + + bulkDeleteOutlets: async (outletIds) => { + try { + const response = await api.delete(`${ENDPOINTS.OUTLETS}/bulk`, { + data: { ids: outletIds } + }); + return response.data; + } catch (error) { + console.error('Error bulk deleting outlets:', error); + throw error; + } + }, +}; + +export default outletsApi; diff --git a/src/style/scss/components/_tables.scss b/src/style/scss/components/_tables.scss index 41b2e9e..c84c449 100644 --- a/src/style/scss/components/_tables.scss +++ b/src/style/scss/components/_tables.scss @@ -683,8 +683,3 @@ table { .custom-table .ant-table-thead .ant-table-cell { background-color: #fafbfe; } - -.custom-table .ant-table-tbody > tr > td { - padding-top: 1px; - padding-bottom: 1px; -} diff --git a/src/style/scss/pages/_stocks.scss b/src/style/scss/pages/_stocks.scss index b9a8a43..b92c32e 100644 --- a/src/style/scss/pages/_stocks.scss +++ b/src/style/scss/pages/_stocks.scss @@ -256,13 +256,12 @@ table { } } .custom-modal-header { - background: $body-bg; padding: 24px; .page-title { h4 { font-size: $font-size-18; font-weight: $font-weight-bold; - color: $secondary; + color: $primary; } } }