add superadmin authentication

This commit is contained in:
ferdiansyah783 2025-08-03 11:14:55 +07:00
parent a40a1994e5
commit 0656cda2ec
22 changed files with 2887 additions and 791 deletions

View File

@ -1,19 +1,16 @@
import React, { useState } from "react"; import { useState } from "react";
import Scrollbars from "react-custom-scrollbars-2"; import Scrollbars from "react-custom-scrollbars-2";
// import { useSelector } from "react-redux";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
import { SidebarData } from "../../core/json/siderbar_data"; import { SidebarData } from "../../core/json/siderbar_data";
import HorizontalSidebar from "./horizontalSidebar";
import CollapsedSidebar from "./collapsedSidebar"; import CollapsedSidebar from "./collapsedSidebar";
import HorizontalSidebar from "./horizontalSidebar";
import { useSelector } from "react-redux";
const Sidebar = () => { const Sidebar = () => {
// const SidebarData = useSelector((state) => state.sidebar_data); const { user } = useSelector((state) => state.auth);
// console.log(sidebarData, "sidebar");
const Location = useLocation(); const Location = useLocation();
console.log("Location.pathname", Location.pathname);
const [subOpen, setSubopen] = useState(""); const [subOpen, setSubopen] = useState("");
const [subsidebar, setSubsidebar] = useState(""); const [subsidebar, setSubsidebar] = useState("");
@ -46,6 +43,12 @@ const Sidebar = () => {
<ul> <ul>
{mainLabel?.submenuItems?.map((title, i) => { {mainLabel?.submenuItems?.map((title, i) => {
if (title?.role) {
if (user?.role !== title?.role) {
return null;
}
}
let link_array = []; let link_array = [];
title?.submenuItems?.map((link) => { title?.submenuItems?.map((link) => {
link_array?.push(link?.link); link_array?.push(link?.link);

View File

@ -54,7 +54,7 @@ export const all_routes = {
floatinglabel: "/form-floating-labels", floatinglabel: "/form-floating-labels",
formvalidation: "/form-validation", formvalidation: "/form-validation",
select2: "/form-select2", select2: "/form-select2",
companylist: "/company-list",
toasts: "/ui-toasts", toasts: "/ui-toasts",
video: "/ui-video", video: "/ui-video",
sweetalerts: "/ui-sweetalerts", sweetalerts: "/ui-sweetalerts",
@ -81,7 +81,7 @@ export const all_routes = {
typicons: "/icon-typicon", typicons: "/icon-typicon",
flagicons: "/icon-flag", flagicons: "/icon-flag",
ribbon: "/ui-ribbon", ribbon: "/ui-ribbon",
paymentmethodlist: "/payment-method-list",
chat: "/chat", chat: "/chat",
videocall: "/video-call", videocall: "/video-call",
audiocall: "/audio-call", audiocall: "/audio-call",

View File

@ -1,18 +1,18 @@
import React from "react"; import { useSelector } from "react-redux";
import { Route, Routes } from "react-router-dom"; import { Outlet, Route, Routes } from "react-router-dom";
import Header from "../InitialPage/Sidebar/Header"; import Header from "../InitialPage/Sidebar/Header";
import Sidebar from "../InitialPage/Sidebar/Sidebar"; import Sidebar from "../InitialPage/Sidebar/Sidebar";
import { pagesRoute, posRoutes, publicRoutes } from "./router.link";
import { Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
import ThemeSettings from "../InitialPage/themeSettings"; import ThemeSettings from "../InitialPage/themeSettings";
import ProtectedRoute from "../components/ProtectedRoute"; import ProtectedRoute from "../components/ProtectedRoute";
import { pagesRoute, posRoutes, publicRoutes } from "./router.link";
// import CollapsedSidebar from "../InitialPage/Sidebar/collapsedSidebar"; // import CollapsedSidebar from "../InitialPage/Sidebar/collapsedSidebar";
import GuestRoute from "../components/GuestRoute";
import Loader from "../feature-module/loader/loader"; import Loader from "../feature-module/loader/loader";
// import HorizontalSidebar from "../InitialPage/Sidebar/horizontalSidebar"; // import HorizontalSidebar from "../InitialPage/Sidebar/horizontalSidebar";
//import LoadingSpinner from "../InitialPage/Sidebar/LoadingSpinner"; //import LoadingSpinner from "../InitialPage/Sidebar/LoadingSpinner";
const AllRoutes = () => { const AllRoutes = () => {
const { user } = useSelector((state) => state.auth);
const data = useSelector((state) => state.toggle_header); const data = useSelector((state) => state.toggle_header);
// const layoutStyles = useSelector((state) => state.layoutstyledata); // const layoutStyles = useSelector((state) => state.layoutstyledata);
const HeaderLayout = () => ( const HeaderLayout = () => (
@ -57,6 +57,7 @@ const AllRoutes = () => {
<Route path={route.path} element={route.element} key={id} /> <Route path={route.path} element={route.element} key={id} />
))} ))}
</Route> </Route>
<Route <Route
path={"/"} path={"/"}
element={ element={
@ -65,12 +66,20 @@ const AllRoutes = () => {
</ProtectedRoute> </ProtectedRoute>
} }
> >
{publicRoutes.map((route, id) => ( {publicRoutes.map((route, id) => {
<Route path={route.path} element={route.element} key={id} /> if (route?.role && route?.role !== user?.role) return null;
))} return <Route path={route.path} element={route.element} key={id} />;
})}
</Route> </Route>
<Route path={"/"} element={<Authpages />}> <Route
path={"/"}
element={
<GuestRoute>
<Authpages />
</GuestRoute>
}
>
{pagesRoute.map((route, id) => ( {pagesRoute.map((route, id) => (
<Route path={route.path} element={route.element} key={id} /> <Route path={route.path} element={route.element} key={id} />
))} ))}

View File

@ -204,6 +204,9 @@ import AddWeddingGuest from "../feature-module/inventory/addWeddingGuest";
import ProductList2 from "../feature-module/inventory/productlist2"; import ProductList2 from "../feature-module/inventory/productlist2";
import ProductList3 from "../feature-module/inventory/productlist3"; import ProductList3 from "../feature-module/inventory/productlist3";
import { all_routes } from "./all_routes"; import { all_routes } from "./all_routes";
import PaymentMethodList from "../feature-module/FinanceAccounts/paymentmethodlist";
import CompanyList from "../feature-module/superadmin/companylist";
export const publicRoutes = [ export const publicRoutes = [
{ {
id: 1, id: 1,
@ -1488,7 +1491,23 @@ export const publicRoutes = [
element: <Navigate to="/" />, element: <Navigate to="/" />,
route: Route, route: Route,
}, },
{
id: 120,
path: routes.paymentmethodlist,
name: "paymendMethodList",
element: <PaymentMethodList /> ,
route: Route,
},
{
id: 1,
path: routes.companylist,
name: "companies",
element: <CompanyList />,
route: Route,
role: 'superadmin'
}
]; ];
export const posRoutes = [ export const posRoutes = [
{ {
id: 1, id: 1,

View File

@ -0,0 +1,15 @@
import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
const GuestRoute = ({ children }) => {
const authState = useSelector((state) => state.auth);
const isAuthenticated = authState?.isAuthenticated || authState?.token;
if (isAuthenticated) {
return <Navigate to="/" replace />;
}
return children;
};
export default GuestRoute;

View File

@ -1,17 +1,12 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children }) => { const ProtectedRoute = ({ children }) => {
// Check if user is authenticated using Redux state // Check if user is authenticated using Redux state
const authState = useSelector((state) => state.auth); const authState = useSelector((state) => state.auth);
const isAuthenticated = authState?.isAuthenticated || authState?.token; const isAuthenticated = authState?.isAuthenticated || authState?.token;
// Fallback to localStorage check if (!isAuthenticated) {
const localStorageAuth = localStorage.getItem('authToken') || localStorage.getItem('user');
const isUserAuthenticated = isAuthenticated || localStorageAuth;
if (!isUserAuthenticated) {
// Redirect to login page if not authenticated // Redirect to login page if not authenticated
return <Navigate to="/signin" replace />; return <Navigate to="/signin" replace />;
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
import { organizationsApi } from '../../../services/organizationsApi';
// Action Types
export const ORGANIZATION_ACTIONS = {
FETCH_ORGANIZATIONS_REQUEST: 'FETCH_ORGANIZATIONS_REQUEST',
FETCH_ORGANIZATIONS_SUCCESS: 'FETCH_ORGANIZATIONS_SUCCESS',
FETCH_ORGANIZATIONS_FAILURE: 'FETCH_ORGANIZATIONS_FAILURE',
FETCH_ORGANIZATION_REQUEST: 'FETCH_ORGANIZATION_REQUEST',
FETCH_ORGANIZATION_SUCCESS: 'FETCH_ORGANIZATION_SUCCESS',
FETCH_ORGANIZATION_FAILURE: 'FETCH_ORGANIZATION_FAILURE',
CREATE_ORGANIZATION_REQUEST: 'CREATE_ORGANIZATION_REQUEST',
CREATE_ORGANIZATION_SUCCESS: 'CREATE_ORGANIZATION_SUCCESS',
CREATE_ORGANIZATION_FAILURE: 'CREATE_ORGANIZATION_FAILURE',
UPDATE_ORGANIZATION_REQUEST: 'UPDATE_ORGANIZATION_REQUEST',
UPDATE_ORGANIZATION_SUCCESS: 'UPDATE_ORGANIZATION_SUCCESS',
UPDATE_ORGANIZATION_FAILURE: 'UPDATE_ORGANIZATION_FAILURE',
DELETE_ORGANIZATION_REQUEST: 'DELETE_ORGANIZATION_REQUEST',
DELETE_ORGANIZATION_SUCCESS: 'DELETE_ORGANIZATION_SUCCESS',
DELETE_ORGANIZATION_FAILURE: 'DELETE_ORGANIZATION_FAILURE',
CLEAR_ORGANIZATION_ERROR: 'CLEAR_ORGANIZATION_ERROR',
CLEAR_CURRENT_ORGANIZATION: 'CLEAR_CURRENT_ORGANIZATION',
};
// Action Creators
export const fetchOrganizations = (params = {}) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_REQUEST });
try {
const data = await organizationsApi.getAllOrganizations(params);
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch organizations',
});
throw error;
}
};
export const fetchOrganization = (id) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_REQUEST });
try {
const data = await organizationsApi.getOrganizationById(id);
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to fetch organization',
});
throw error;
}
};
export const createOrganization = (organizationData) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_REQUEST });
try {
const data = await organizationsApi.createOrganization(organizationData);
dispatch({
type: ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to create organization',
});
throw error;
}
};
export const updateOrganization = (id, organizationData) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_REQUEST });
try {
const data = await organizationsApi.updateOrganization(id, organizationData);
dispatch({
type: ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to update organization',
});
throw error;
}
};
export const deleteOrganization = (id) => async (dispatch) => {
dispatch({ type: ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_REQUEST });
try {
await organizationsApi.deleteOrganization(id);
dispatch({
type: ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_FAILURE,
payload: error.response?.data?.message || error.message || 'Failed to delete organization',
});
throw error;
}
};
export const clearOrganizationError = () => ({
type: ORGANIZATION_ACTIONS.CLEAR_ORGANIZATION_ERROR,
});
export const clearCurrentOrganization = () => ({
type: ORGANIZATION_ACTIONS.CLEAR_CURRENT_ORGANIZATION,
});

View File

@ -0,0 +1,126 @@
import { paymentMethodsApi } from '../../../services/paymentMethodsApi';
export const PAYMENT_METHOD_ACTIONS = {
FETCH_PAYMENT_METHODS_REQUEST: 'FETCH_PAYMENT_METHODS_REQUEST',
FETCH_PAYMENT_METHODS_SUCCESS: 'FETCH_PAYMENT_METHODS_SUCCESS',
FETCH_PAYMENT_METHODS_FAILURE: 'FETCH_PAYMENT_METHODS_FAILURE',
FETCH_PAYMENT_METHOD_REQUEST: 'FETCH_PAYMENT_METHOD_REQUEST',
FETCH_PAYMENT_METHOD_SUCCESS: 'FETCH_PAYMENT_METHOD_SUCCESS',
FETCH_PAYMENT_METHOD_FAILURE: 'FETCH_PAYMENT_METHOD_FAILURE',
CREATE_PAYMENT_METHOD_REQUEST: 'CREATE_PAYMENT_METHOD_REQUEST',
CREATE_PAYMENT_METHOD_SUCCESS: 'CREATE_PAYMENT_METHOD_SUCCESS',
CREATE_PAYMENT_METHOD_FAILURE: 'CREATE_PAYMENT_METHOD_FAILURE',
UPDATE_PAYMENT_METHOD_REQUEST: 'UPDATE_PAYMENT_METHOD_REQUEST',
UPDATE_PAYMENT_METHOD_SUCCESS: 'UPDATE_PAYMENT_METHOD_SUCCESS',
UPDATE_PAYMENT_METHOD_FAILURE: 'UPDATE_PAYMENT_METHOD_FAILURE',
DELETE_PAYMENT_METHOD_REQUEST: 'DELETE_PAYMENT_METHOD_REQUEST',
DELETE_PAYMENT_METHOD_SUCCESS: 'DELETE_PAYMENT_METHOD_SUCCESS',
DELETE_PAYMENT_METHOD_FAILURE: 'DELETE_PAYMENT_METHOD_FAILURE',
CLEAR_PAYMENT_METHOD_ERROR: 'CLEAR_PAYMENT_METHOD_ERROR',
CLEAR_CURRENT_PAYMENT_METHOD: 'CLEAR_CURRENT_PAYMENT_METHOD',
};
// Action Creators
export const fetchPaymentMethods = (params = {}) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_REQUEST });
try {
const data = await paymentMethodsApi.getAll(params);
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const fetchPaymentMethod = (id) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_REQUEST });
try {
const data = await paymentMethodsApi.getById(id);
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const createPaymentMethod = (formData) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_REQUEST });
try {
const data = await paymentMethodsApi.create(formData);
dispatch({
type: PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_SUCCESS,
payload: data,
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const updatePaymentMethod = (id, formData) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_REQUEST });
try {
const data = await paymentMethodsApi.update(id, formData);
dispatch({
type: PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_SUCCESS,
payload: { id, data },
});
return data;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const deletePaymentMethod = (id) => async (dispatch) => {
dispatch({ type: PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_REQUEST });
try {
await paymentMethodsApi.remove(id);
dispatch({
type: PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_SUCCESS,
payload: id,
});
return id;
} catch (error) {
dispatch({
type: PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_FAILURE,
payload: error.response?.data?.message || error.message,
});
throw error;
}
};
export const clearPaymentMethodError = () => ({
type: PAYMENT_METHOD_ACTIONS.CLEAR_PAYMENT_METHOD_ERROR,
});
export const clearCurrentPaymentMethod = () => ({
type: PAYMENT_METHOD_ACTIONS.CLEAR_CURRENT_PAYMENT_METHOD,
});

View File

@ -4,6 +4,8 @@ import productReducer from './reducers/productReducer';
import authReducer from './reducers/authReducer'; import authReducer from './reducers/authReducer';
import categoryReducer from './reducers/categoryReducer'; import categoryReducer from './reducers/categoryReducer';
import orderReducer from './reducers/orderReducer'; import orderReducer from './reducers/orderReducer';
import paymentMethodReducer from './reducers/paymentMethodReducer';
import organizationReducer from './reducers/organizationReducer';
// Legacy reducer for existing functionality // Legacy reducer for existing functionality
const legacyReducer = (state = initialState, action) => { const legacyReducer = (state = initialState, action) => {
@ -79,6 +81,8 @@ const rootReducer = combineReducers({
auth: authReducer, auth: authReducer,
categories: categoryReducer, categories: categoryReducer,
orders: orderReducer, orders: orderReducer,
paymentMethods: paymentMethodReducer,
organizations: organizationReducer
}); });
export default rootReducer; export default rootReducer;

View File

@ -0,0 +1,212 @@
import { ORGANIZATION_ACTIONS } from '../actions/organizationActions';
const initialState = {
// Organizations list
organizations: [],
totalOrganizations: 0,
currentPage: 1,
totalPages: 1,
pageSize: 10,
hasPrevious: false,
hasNext: false,
// Current organization (for edit/view)
currentOrganization: null,
// Search results
searchResults: [],
searchQuery: '',
// Loading states
loading: false,
organizationLoading: false,
searchLoading: false,
// Error states
error: null,
organizationError: null,
searchError: null,
// Operation states
creating: false,
updating: false,
deleting: false,
};
const organizationReducer = (state = initialState, action) => {
switch (action.type) {
// Fetch Organizations
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_REQUEST:
return {
...state,
loading: true,
error: null,
};
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_SUCCESS: {
const { organizations, total_count, page, total_pages, limit } = action.payload.data;
return {
...state,
loading: false,
organizations: organizations,
totalOrganizations: total_count || organizations.length,
currentPage: page || 1,
totalPages: total_pages || 1,
pageSize: limit || 10,
hasPrevious: false,
hasNext: false,
error: null,
};
}
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATIONS_FAILURE:
return {
...state,
loading: false,
error: action.payload,
};
// Fetch Single Organization
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_REQUEST:
return {
...state,
organizationLoading: true,
organizationError: null,
};
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_SUCCESS:
return {
...state,
organizationLoading: false,
currentOrganization: action.payload.data,
organizationError: null,
};
case ORGANIZATION_ACTIONS.FETCH_ORGANIZATION_FAILURE:
return {
...state,
organizationLoading: false,
organizationError: action.payload,
};
// Create Organization
case ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_REQUEST:
return {
...state,
creating: true,
error: null,
};
case ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_SUCCESS:
return {
...state,
creating: false,
organizations: [action.payload.data, ...state.organizations],
totalOrganizations: state.totalOrganizations + 1,
error: null,
};
case ORGANIZATION_ACTIONS.CREATE_ORGANIZATION_FAILURE:
return {
...state,
creating: false,
error: action.payload,
};
// Update Organization
case ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_REQUEST:
return {
...state,
updating: true,
error: null,
};
case ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_SUCCESS:
return {
...state,
updating: false,
organizations: state.organizations.map(org =>
org.id === action.payload.data.id ? action.payload.data : org
),
currentOrganization: action.payload.data,
error: null,
};
case ORGANIZATION_ACTIONS.UPDATE_ORGANIZATION_FAILURE:
return {
...state,
updating: false,
error: action.payload,
};
// Delete Organization
case ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_REQUEST:
return {
...state,
deleting: true,
error: null,
};
case ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_SUCCESS:
return {
...state,
deleting: false,
organizations: state.organizations.filter(org => org.id !== action.payload),
totalOrganizations: state.totalOrganizations - 1,
error: null,
};
case ORGANIZATION_ACTIONS.DELETE_ORGANIZATION_FAILURE:
return {
...state,
deleting: false,
error: action.payload,
};
// Search Organizations
case ORGANIZATION_ACTIONS.SEARCH_ORGANIZATIONS_REQUEST:
return {
...state,
searchLoading: true,
searchError: null,
};
case ORGANIZATION_ACTIONS.SEARCH_ORGANIZATIONS_SUCCESS:
return {
...state,
searchLoading: false,
searchResults: action.payload.data || action.payload,
searchQuery: action.payload.query || '',
searchError: null,
};
case ORGANIZATION_ACTIONS.SEARCH_ORGANIZATIONS_FAILURE:
return {
...state,
searchLoading: false,
searchError: action.payload,
};
// Clear States
case ORGANIZATION_ACTIONS.CLEAR_ORGANIZATION_ERROR:
return {
...state,
error: null,
organizationError: null,
searchError: null,
};
case ORGANIZATION_ACTIONS.CLEAR_CURRENT_ORGANIZATION:
return {
...state,
currentOrganization: null,
organizationError: null,
};
default:
return state;
}
};
export default organizationReducer;

View File

@ -0,0 +1,106 @@
import { PAYMENT_METHOD_ACTIONS } from '../actions/paymentMethodActions';
const initialState = {
paymentMethods: [],
totalPaymentMethods: 0,
currentPage: 1,
totalPages: 1,
pageSize: 10,
currentPaymentMethod: null,
loading: false,
creating: false,
updating: false,
deleting: false,
error: null,
detailError: null,
};
const paymentMethodReducer = (state = initialState, action) => {
switch (action.type) {
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_REQUEST:
return { ...state, loading: true, error: null };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_SUCCESS: {
const { payment_methods, total_count, page, total_pages, limit } = action.payload.data;
return {
...state,
loading: false,
paymentMethods: payment_methods,
totalPaymentMethods: total_count || payment_methods.length,
currentPage: page || 1,
totalPages: total_pages || 1,
pageSize: limit || 10,
};
}
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHODS_FAILURE:
return { ...state, loading: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_REQUEST:
return { ...state, detailLoading: true, detailError: null };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_SUCCESS:
return { ...state, detailLoading: false, currentPaymentMethod: action.payload.data };
case PAYMENT_METHOD_ACTIONS.FETCH_PAYMENT_METHOD_FAILURE:
return { ...state, detailLoading: false, detailError: action.payload };
case PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_REQUEST:
return { ...state, creating: true };
case PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_SUCCESS:
return {
...state,
creating: false,
paymentMethods: [action.payload.data, ...state.paymentMethods],
totalPaymentMethods: state.totalPaymentMethods + 1,
};
case PAYMENT_METHOD_ACTIONS.CREATE_PAYMENT_METHOD_FAILURE:
return { ...state, creating: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_REQUEST:
return { ...state, updating: true };
case PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_SUCCESS:
return {
...state,
updating: false,
paymentMethods: state.paymentMethods.map((item) =>
item.id === action.payload.data.id ? action.payload.data : item
),
currentPaymentMethod: action.payload.data,
};
case PAYMENT_METHOD_ACTIONS.UPDATE_PAYMENT_METHOD_FAILURE:
return { ...state, updating: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_REQUEST:
return { ...state, deleting: true };
case PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_SUCCESS:
return {
...state,
deleting: false,
paymentMethods: state.paymentMethods.filter((item) => item.id !== action.payload),
totalPaymentMethods: state.totalPaymentMethods - 1,
};
case PAYMENT_METHOD_ACTIONS.DELETE_PAYMENT_METHOD_FAILURE:
return { ...state, deleting: false, error: action.payload };
case PAYMENT_METHOD_ACTIONS.CLEAR_PAYMENT_METHOD_ERROR:
return { ...state, error: null, detailError: null };
case PAYMENT_METHOD_ACTIONS.CLEAR_CURRENT_PAYMENT_METHOD:
return { ...state, currentPaymentMethod: null };
default:
return state;
}
};
export default paymentMethodReducer;

View File

@ -0,0 +1,387 @@
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 { deletePaymentMethod, fetchPaymentMethod, fetchPaymentMethods } from "../../core/redux/actions/paymentMethodActions";
import { formatDate } from "../../utils/date";
const PaymentMethodList = () => {
const {
paymentMethods: apiPaymentMethods,
loading,
error,
totalPaymentMethods,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.paymentMethods);
const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header);
const dataSource = apiPaymentMethods?.length > 0 ? apiPaymentMethods : [];
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadPaymentMethods = 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(fetchPaymentMethods(cleanParams));
} catch (error) {
console.error("Failed to load categories", error);
}
};
loadPaymentMethods();
}, [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 = totalPaymentMethods || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
const handleDeletePaymentMethod = async (paymentMethodId) => {
try {
await dispatch(deletePaymentMethod(paymentMethodId));
// Show success message
MySwal.fire({
title: "Deleted!",
text: "Payment Method has been deleted successfully.",
icon: "success",
className: "btn btn-success",
customClass: {
confirmButton: "btn btn-success",
},
});
} catch (error) {
console.error("Failed to delete payment method:", error);
MySwal.fire({
title: "Error!",
text: "Failed to delete payment method. Please try again.",
icon: "error",
className: "btn btn-danger",
customClass: {
confirmButton: "btn btn-danger",
},
});
}
};
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
);
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: "Payment Method",
dataIndex: "paymentmethod",
render: (_, record) => {
return <span>{record.name}</span>;
},
sorter: (a, b) => a.name.length - b.name.length,
},
{
title: "Type",
dataIndex: "type",
render: (_, record) => {
return <span>{record?.type}</span>;
},
sorter: (a, b) => a.type.length - b.type.length,
},
{
title: "Created On",
dataIndex: "createdon",
render: (_, record) => {
return <span>{formatDate(record.created_at)}</span>;
},
sorter: (a, b) => a.created_at.length - b.created_at.length,
},
{
title: "Status",
dataIndex: "status",
render: (_, record) => (
<Tag color="#87d068">{record.is_active ? "Active" : "Inactive"}</Tag>
),
sorter: (a, b) => a.status.length - b.status.length,
},
{
title: "Actions",
dataIndex: "actions",
key: "actions",
render: (_, record) => (
<td className="action-table-data">
<div className="edit-delete-action">
<Link
className="me-2 p-2"
to="#"
data-bs-toggle="modal"
data-bs-target="#edit-category"
onClick={() => dispatch(fetchPaymentMethod(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<Link
className="confirm-text p-2"
to="#"
onClick={(e) => {
e.preventDefault();
MySwal.fire({
title: "Are you sure?",
text: "You won't be able to revert this!",
showCancelButton: true,
confirmButtonColor: "#00ff00",
confirmButtonText: "Yes, delete it!",
cancelButtonColor: "#ff0000",
cancelButtonText: "Cancel",
}).then((result) => {
if (result.isConfirmed) {
handleDeletePaymentMethod(record.id || record.key);
}
});
}}
>
<Trash2 className="feather-trash-2" />
</Link>
</div>
</td>
),
},
];
const MySwal = withReactContent(Swal);
return (
<div>
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Payment Method</h4>
<h6>Manage your payment methods</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<ImageWithBasePath
src="assets/img/icons/pdf.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderExcelTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<ImageWithBasePath
src="assets/img/icons/excel.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderPrinterTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<i data-feather="printer" className="feather-printer" />
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderCollapseTooltip}>
<Link
data-bs-toggle="tooltip"
data-bs-placement="top"
id="collapse-header"
className={data ? "active" : ""}
onClick={() => {
dispatch(setToogleHeader(!data));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
type="text"
placeholder="Search"
className="form-control form-control-sm formsearch"
onChange={handleSearch}
/>
<Link to className="btn btn-searchset">
<i data-feather="search" className="feather-search" />
</Link>
</div>
</div>
<Select
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
<div className="table-responsive">
{loading ? (
<div className="text-center p-4">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-2">Loading categories...</p>
</div>
) : error ? (
<div className="alert alert-danger" role="alert">
<strong>Error:</strong> {error}
<button
className="btn btn-sm btn-outline-danger ms-2"
onClick={() => dispatch(fetchPaymentMethods())}
>
Retry
</button>
</div>
) : (
<>
<Table columns={columns} dataSource={dataSource} />
<CustomPagination
currentPage={currentPage}
pageSize={pageSize}
totalCount={totalRecords}
totalPages={actualTotalPages}
loading={loading}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
pageSizeOptions={[10, 20, 50, 100]}
showInfo={true}
showPageSizeSelector={true}
compact={false}
className="paymentmethod-list-pagination"
/>
</>
)}
</div>
</div>
</div>
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
);
};
export default PaymentMethodList;

View File

@ -11,7 +11,7 @@ import {
Plus, Plus,
PlusCircle, PlusCircle,
Trash2, Trash2,
X X,
} from "feather-icons-react/build/IconComponents"; } from "feather-icons-react/build/IconComponents";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { OverlayTrigger, Tooltip } from "react-bootstrap";
@ -180,12 +180,19 @@ const AddProduct = () => {
} }
try { try {
// Prepare the data for submission const isAllEmpty = variants.every(
(item) =>
item.name === "" && item.price_modifier === 0 && item.cost === 0
);
const productData = { const productData = {
...formData, ...formData,
variants
}; };
if (!isAllEmpty) {
productData.variants = variants;
}
// Remove empty values // Remove empty values
const cleanData = Object.fromEntries( const cleanData = Object.fromEntries(
Object.entries(productData).filter(([, value]) => { Object.entries(productData).filter(([, value]) => {
@ -222,7 +229,7 @@ const AddProduct = () => {
Swal.fire({ Swal.fire({
icon: "error", icon: "error",
title: "Error!", title: "Error!",
text: error.message || "Failed to create product. Please try again.", text: error?.response?.data?.errors[0].cause || "Failed to create product. Please try again.",
}); });
} }
}; };
@ -254,7 +261,7 @@ const AddProduct = () => {
const handleChangeVariant = (index, field, value) => { const handleChangeVariant = (index, field, value) => {
const newVariants = [...variants]; const newVariants = [...variants];
if (['price_modifier', 'cost'].includes(field)) value = Number(value); if (["price_modifier", "cost"].includes(field)) value = Number(value);
newVariants[index][field] = value; newVariants[index][field] = value;
setVariants(newVariants); setVariants(newVariants);
}; };
@ -683,6 +690,7 @@ const AddProduct = () => {
</ul> </ul>
</div> </div>
</div> </div>
<div className="tab-content" id="pills-tabContent"> <div className="tab-content" id="pills-tabContent">
<div <div
className="tab-pane fade show active" className="tab-pane fade show active"
@ -872,7 +880,10 @@ const AddProduct = () => {
className="btn btn-primary mt-2" className="btn btn-primary mt-2"
onClick={addVariant} onClick={addVariant}
> >
<Plus data-feather="plus" className="me-1 icon-small" /> <Plus
data-feather="plus"
className="me-1 icon-small"
/>
Add Variant Add Variant
</button> </button>
</div> </div>

View File

@ -188,12 +188,19 @@ const EditProduct = () => {
} }
try { try {
// Prepare the data for submission const isAllEmpty = variants.every(
(item) =>
item.name === "" && item.price_modifier === 0 && item.cost === 0
);
const productData = { const productData = {
...formData, ...formData,
variants,
}; };
if (!isAllEmpty) {
productData.variants = variants;
}
// Remove empty values // Remove empty values
const cleanData = Object.fromEntries( const cleanData = Object.fromEntries(
Object.entries(productData).filter(([, value]) => { Object.entries(productData).filter(([, value]) => {

View File

@ -12,27 +12,26 @@ const Signin = () => {
const authState = useSelector((state) => state.auth); const authState = useSelector((state) => state.auth);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: '', email: "",
password: '' password: "",
}); });
const [error, setError] = useState(''); const [error, setError] = useState("");
const handleInputChange = (e) => { const handleInputChange = (e) => {
setFormData({ setFormData({
...formData, ...formData,
[e.target.name]: e.target.value [e.target.name]: e.target.value,
}); });
}; };
const handleSubmit = async (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
setError(''); setError("");
try { try {
await authApi.login(formData); authApi.login(formData).then(() => navigate(route.dashboard));
navigate(route.dashboard);
} catch (error) { } catch (error) {
setError(error.message || 'Login failed'); setError(error.message || "Login failed");
} }
}; };
@ -115,7 +114,7 @@ const Signin = () => {
className="btn btn-login" className="btn btn-login"
disabled={authState.loading} disabled={authState.loading}
> >
{authState.loading ? 'Signing In...' : 'Sign In'} {authState.loading ? "Signing In..." : "Sign In"}
</button> </button>
</div> </div>
<div className="signinform"> <div className="signinform">

View File

@ -1,4 +1,4 @@
import { DatePicker, Space, Select as AntSelect } from "antd"; import { Select as AntSelect, DatePicker, Space } from "antd";
import { import {
ChevronUp, ChevronUp,
PlusCircle, PlusCircle,
@ -17,7 +17,7 @@ import {
fetchOrders, fetchOrders,
} from "../../core/redux/actions/orderActions"; } from "../../core/redux/actions/orderActions";
import { formatRupiah } from "../../utils/currency"; import { formatRupiah } from "../../utils/currency";
import { formatDate } from "../../utils/date"; import { formatDate, formatInputDate } from "../../utils/date";
const SalesList = () => { const SalesList = () => {
const { const {
@ -35,25 +35,41 @@ const SalesList = () => {
const dataSource = apiOrders?.length > 0 ? apiOrders : []; const dataSource = apiOrders?.length > 0 ? apiOrders : [];
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1); const [params, setParams] = useState({
const [pageSize, setPageSize] = useState(reduxPageSize || 10); page: reduxCurrentPage || 1,
limit: reduxPageSize || 10,
status: null,
date_from: null,
date_to: null,
});
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [orderStatus, setOrderStatus] = useState(null);
const [selectedOrder, setSelectedOrder] = useState(null);
const handleSetParams = (key, value) => {
setParams({
...params,
[key]: value,
});
};
useEffect(() => { useEffect(() => {
const loadOrders = async () => { const loadOrders = async () => {
try { try {
const searchParams = { const receivedParams = {
page: currentPage, page: params.page,
limit: pageSize, limit: params.limit,
search: debouncedSearchTerm || "", search: debouncedSearchTerm || "",
status: orderStatus, status: params.status,
date_from: params.date_from,
date_to: params.date_to,
}; };
// Remove empty parameters // Remove empty parameters
const cleanParams = Object.fromEntries( const cleanParams = Object.fromEntries(
Object.entries(searchParams).filter(([, value]) => value !== "") Object.entries(receivedParams).filter(([, value]) => value !== "")
); );
await dispatch(fetchOrders(cleanParams)); await dispatch(fetchOrders(cleanParams));
@ -63,7 +79,7 @@ const SalesList = () => {
}; };
loadOrders(); loadOrders();
}, [dispatch, currentPage, pageSize, debouncedSearchTerm, orderStatus]); }, [dispatch, params, debouncedSearchTerm]);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -78,30 +94,23 @@ const SalesList = () => {
const value = e.target.value; const value = e.target.value;
setSearchTerm(value); setSearchTerm(value);
// Reset to first page when searching // Reset to first page when searching
setCurrentPage(1); handleSetParams("page", 1);
};
const handleFilterStatus = (e) => {
const value = e.target.value;
setOrderStatus(value);
setCurrentPage(1);
}; };
// Handle pagination // Handle pagination
const handlePageChange = (page) => { const handlePageChange = (page) => {
setCurrentPage(page); handleSetParams("page", page);
}; };
// Handle page size change // Handle page size change
const handlePageSizeChange = (newPageSize) => { const handlePageSizeChange = (newPageSize) => {
setPageSize(newPageSize); handleSetParams("limit", newPageSize);
setCurrentPage(1); // Reset to first page when changing page size handleSetParams("page", 1);
}; };
// Calculate pagination info // Calculate pagination info
const totalRecords = totalOrders || dataSource.length; const totalRecords = totalOrders || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize); const calculatedTotalPages = Math.ceil(totalRecords / params.limit);
const actualTotalPages = totalPages || calculatedTotalPages; const actualTotalPages = totalPages || calculatedTotalPages;
// Clear error when component unmounts // Clear error when component unmounts
@ -141,13 +150,6 @@ const SalesList = () => {
cancelled: "danger", cancelled: "danger",
}; };
const options = [
{ 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 [selectedDate, setSelectedDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(new Date());
const handleDateChange = (date) => { const handleDateChange = (date) => {
setSelectedDate(date); setSelectedDate(date);
@ -272,23 +274,48 @@ const SalesList = () => {
</div> </div>
</div> </div>
<Space warp> <Space warp align="center">
<AntSelect <AntSelect
style={{ height: 36, width: 120 }} style={{ height: 36, width: 120 }}
placeholder={"Status"} placeholder={"Status"}
options={paymentStatus} options={paymentStatus}
value={ value={
paymentStatus.find( paymentStatus.find(
(option) => option.value === orderStatus (option) => option.value === params.status
) || null ) || null
} }
onChange={handleFilterStatus} onChange={(selectedOption) =>
handleSetParams("status", selectedOption)
}
/> />
<AntSelect <DatePicker
selected={params.date_from}
onChange={(date) =>
date
? handleSetParams("date_from", formatInputDate(date))
: handleSetParams("date_from", "")
}
height={120}
type="date"
className="datetimepicker w-100"
dateFormat="dd-MM-yyyy"
placeholder="From Date"
style={{ height: 36 }}
/>
<DatePicker
selected={params.date_to}
onChange={(date) =>
date
? handleSetParams("date_to", formatInputDate(date))
: handleSetParams("date_to", "")
}
type="date"
className="datetimepicker w-100"
dateFormat="dd-MM-yyyy"
placeholder="To Date"
style={{ height: 36 }} style={{ height: 36 }}
defaultValue={options[0]?.value}
options={options}
/> />
</Space> </Space>
</div> </div>
@ -376,12 +403,13 @@ const SalesList = () => {
className="dropdown-item" className="dropdown-item"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#sales-details-new" data-bs-target="#sales-details-new"
onClick={() => setSelectedOrder(item)}
> >
<i <i
data-feather="eye" data-feather="eye"
className="info-img" className="info-img"
/> />
Sale Detail Sales Detail
</Link> </Link>
</li> </li>
<li> <li>
@ -455,8 +483,8 @@ const SalesList = () => {
</table> </table>
<CustomPagination <CustomPagination
currentPage={currentPage} currentPage={params.page}
pageSize={pageSize} pageSize={params.limit}
totalCount={totalRecords} totalCount={totalRecords}
totalPages={actualTotalPages} totalPages={actualTotalPages}
loading={loading} loading={loading}
@ -687,11 +715,13 @@ const SalesList = () => {
</div> </div>
</div> </div>
{/* /add popup */} {/* /add popup */}
{/* details popup */} {/* details popup */}
<div className="modal fade" id="sales-details-new"> <div className="modal fade" id="sales-details-new">
<div className="modal-dialog sales-details-modal"> <div className="modal-dialog sales-details-modal">
<div className="modal-content"> <div className="modal-content">
<div className="page-wrapper details-blk"> <div className="page-wrapper details-blk">
{setSelectedOrder && (
<div className="content p-4"> <div className="content p-4">
<div className="d-flex justify-content-between align-items-center mb-4 modal-header"> <div className="d-flex justify-content-between align-items-center mb-4 modal-header">
<h4 className="fw-bold">Sales Detail</h4> <h4 className="fw-bold">Sales Detail</h4>
@ -703,7 +733,9 @@ const SalesList = () => {
<div className="row g-4 mb-4"> <div className="row g-4 mb-4">
<div className="col-md-4"> <div className="col-md-4">
<h6 className="fw-bold text-muted">Customer Info</h6> <h6 className="fw-bold text-muted">Customer Info</h6>
<p className="mb-0">Carl Evans</p> <p className="mb-0">
{selectedOrder?.metadata?.customer_name}
</p>
<small className="text-muted d-block"> <small className="text-muted d-block">
3103 Trainer Avenue Peoria, IL 61602 3103 Trainer Avenue Peoria, IL 61602
</small> </small>
@ -732,13 +764,21 @@ const SalesList = () => {
<small className="d-block"> <small className="d-block">
Reference:{" "} Reference:{" "}
<span className="text-warning fw-semibold"> <span className="text-warning fw-semibold">
#SL0101 {selectedOrder?.order_number}
</span> </span>
</small> </small>
<small className="d-block">Date: Dec 24, 2024</small> <small className="d-block">
Date: {formatDate(selectedOrder?.created_at)}
</small>
<small className="d-block"> <small className="d-block">
Status:{" "} Status:{" "}
<span className="badge bg-success">Completed</span> <span
className={`badge text-bg-${
badgeColors[selectedOrder?.status]
}`}
>
{selectedOrder?.status}
</span>
</small> </small>
<small className="d-block"> <small className="d-block">
Payment Status:{" "} Payment Status:{" "}
@ -756,109 +796,54 @@ const SalesList = () => {
<thead className="thead-light text-dark"> <thead className="thead-light text-dark">
<tr> <tr>
<th>Product</th> <th>Product</th>
<th>Purchase Price($)</th> <th>Variant</th>
<th>Discount($)</th> <th>Status</th>
<th>Tax(%)</th> <th>Quantity</th>
<th>Tax Amount($)</th> <th>Unit Price</th>
<th>Unit Cost($)</th> <th>Total Price</th>
<th>Total Cost(%)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> {selectedOrder?.order_items?.map((item, index) => (
<td> <tr key={index}>
<img <td>{item?.product_name}</td>
src="/assets/img/products/stock-img-02.png" <td>{item?.product_variant_name ?? "-"}</td>
alt="Nike Jordan" <td>{item?.status}</td>
width="30" <td>{item?.quantity}</td>
className="me-2" <td>{formatRupiah(item?.unit_price)}</td>
/>{" "} <td>{formatRupiah(item?.total_price)}</td>
Nike Jordan
</td>
<td>2000</td>
<td>500</td>
<td>0.00</td>
<td>0.00</td>
<td>0.00</td>
<td>1500</td>
</tr>
<tr>
<td>
<img
src="/assets/img/products/stock-img-03.png"
alt="Apple Watch"
width="30"
className="me-2"
/>{" "}
Apple Series 5 Watch
</td>
<td>3000</td>
<td>400</td>
<td>0.00</td>
<td>0.00</td>
<td>0.00</td>
<td>1700</td>
</tr>
<tr>
<td>
<img
src="/assets/img/products/stock-img-05.png"
alt="Lobar Handy"
width="30"
className="me-2"
/>{" "}
Lobar Handy
</td>
<td>2500</td>
<td>500</td>
<td>0.00</td>
<td>0.00</td>
<td>0.00</td>
<td>2000</td>
</tr> </tr>
))}
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="row justify-content-end"> <div className="row justify-content-end pb-3">
<div className="col-md-6"> <div className="col-md-6">
<table className="table table-bordered"> <table className="table table-bordered">
<tbody> <tbody>
<tr> <tr>
<td>Order Tax</td> <td>Order Tax</td>
<td className="text-end">$ 0.00</td> <td className="text-end">{formatRupiah(selectedOrder?.tax_amount)}</td>
</tr> </tr>
<tr> <tr>
<td>Discount</td> <td>Discount</td>
<td className="text-end">$ 0.00</td> <td className="text-end">{formatRupiah(selectedOrder?.discount_amount)}</td>
</tr> </tr>
<tr className="fw-bold"> <tr className="fw-bold">
<td>Grand Total</td> <td>Grand Total</td>
<td className="text-end">$ 5200.00</td> <td className="text-end">{formatRupiah(selectedOrder?.total_amount)}</td>
</tr> </tr>
<tr> <tr>
<td>Paid</td> <td>Sub Total</td>
<td className="text-end">$ 5200.00</td> <td className="text-end">{formatRupiah(selectedOrder?.subtotal)}</td>
</tr>
<tr>
<td>Due</td>
<td className="text-end">$ 0.00</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div className="d-flex justify-content-end modal-footer">
<button
className="btn btn-outline-primary me-2"
data-bs-dismiss="modal"
>
Cancel
</button>
<button className="btn btn-primary">Submit</button>
</div>
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -894,7 +879,7 @@ const SalesList = () => {
<tr> <tr>
<th>Product</th> <th>Product</th>
<th>Qty</th> <th>Qty</th>
<th>Purchase Price($)</th> <th>Purchase Price</th>
<th>Discount($)</th> <th>Discount($)</th>
<th>Tax(%)</th> <th>Tax(%)</th>
<th>Tax Amount($)</th> <th>Tax Amount($)</th>

View File

@ -0,0 +1,396 @@
import { Select } 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 { fetchOrganizations } from "../../core/redux/actions/organizationActions";
import { deletePaymentMethod, fetchPaymentMethod, fetchPaymentMethods } from "../../core/redux/actions/paymentMethodActions";
import { formatDate } from "../../utils/date";
const CompanyList = () => {
const {
organizations: apiPaymentMethods,
loading,
error,
totalPaymentMethods,
totalPages,
pageSize: reduxPageSize,
currentPage: reduxCurrentPage,
} = useSelector((state) => state.organizations);
const dispatch = useDispatch();
const data = useSelector((state) => state.toggle_header);
const dataSource = apiPaymentMethods?.length > 0 ? apiPaymentMethods : [];
const [currentPage, setCurrentPage] = useState(reduxCurrentPage || 1);
const [pageSize, setPageSize] = useState(reduxPageSize || 10);
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
useEffect(() => {
const loadPaymentMethods = 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(fetchOrganizations(cleanParams));
} catch (error) {
console.error("Failed to load categories", error);
}
};
loadPaymentMethods();
}, [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 = totalPaymentMethods || dataSource.length;
const calculatedTotalPages = Math.ceil(totalRecords / pageSize);
const actualTotalPages = totalPages || calculatedTotalPages;
const handleDeletePaymentMethod = async (paymentMethodId) => {
try {
await dispatch(deletePaymentMethod(paymentMethodId));
// Show success message
MySwal.fire({
title: "Deleted!",
text: "Payment Method has been deleted successfully.",
icon: "success",
className: "btn btn-success",
customClass: {
confirmButton: "btn btn-success",
},
});
} catch (error) {
console.error("Failed to delete payment method:", error);
MySwal.fire({
title: "Error!",
text: "Failed to delete payment method. Please try again.",
icon: "error",
className: "btn btn-danger",
customClass: {
confirmButton: "btn btn-danger",
},
});
}
};
const renderTooltip = (props) => (
<Tooltip id="pdf-tooltip" {...props}>
Pdf
</Tooltip>
);
const renderExcelTooltip = (props) => (
<Tooltip id="excel-tooltip" {...props}>
Excel
</Tooltip>
);
const renderPrinterTooltip = (props) => (
<Tooltip id="printer-tooltip" {...props}>
Printer
</Tooltip>
);
const renderRefreshTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Refresh
</Tooltip>
);
const renderCollapseTooltip = (props) => (
<Tooltip id="refresh-tooltip" {...props}>
Collapse
</Tooltip>
);
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: "Company Name",
dataIndex: "companyname",
render: (_, record) => {
return <span>{record.name}</span>;
},
sorter: (a, b) => a.name.length - b.name.length,
},
{
title: "Email",
dataIndex: "email",
render: (_, record) => {
return <span>{record?.email ?? '-'}</span>;
},
sorter: (a, b) => a.email.length - b.email.length,
},
{
title: "Phone Number",
dataIndex: "phonenumber",
render: (_, record) => {
return <span>{record?.phone_number ?? '-'}</span>;
},
sorter: (a, b) => a.phone_number.length - b.phone_number.length,
},
{
title: "Plan",
dataIndex: "plan",
render: (_, record) => {
return <span>{record?.plan_type ?? '-'}</span>;
},
sorter: (a, b) => a.plan_type.length - b.plan_type.length,
},
{
title: "Created On",
dataIndex: "createdon",
render: (_, record) => {
return <span>{formatDate(record.created_at)}</span>;
},
sorter: (a, b) => a.created_at.length - b.created_at.length,
},
{
title: "Actions",
dataIndex: "actions",
key: "actions",
render: (_, record) => (
<td className="action-table-data">
<div className="edit-delete-action">
<Link
className="me-2 p-2"
to="#"
data-bs-toggle="modal"
data-bs-target="#edit-category"
onClick={() => dispatch(fetchPaymentMethod(record.id))}
>
<i data-feather="edit" className="feather-edit"></i>
</Link>
<Link
className="confirm-text p-2"
to="#"
onClick={(e) => {
e.preventDefault();
MySwal.fire({
title: "Are you sure?",
text: "You won't be able to revert this!",
showCancelButton: true,
confirmButtonColor: "#00ff00",
confirmButtonText: "Yes, delete it!",
cancelButtonColor: "#ff0000",
cancelButtonText: "Cancel",
}).then((result) => {
if (result.isConfirmed) {
handleDeletePaymentMethod(record.id || record.key);
}
});
}}
>
<Trash2 className="feather-trash-2" />
</Link>
</div>
</td>
),
},
];
const MySwal = withReactContent(Swal);
return (
<div>
<div className="page-wrapper">
<div className="content">
<div className="page-header">
<div className="add-item d-flex">
<div className="page-title">
<h4>Companies</h4>
<h6>Manage your companies</h6>
</div>
</div>
<ul className="table-top-head">
<li>
<OverlayTrigger placement="top" overlay={renderTooltip}>
<Link>
<ImageWithBasePath
src="assets/img/icons/pdf.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderExcelTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<ImageWithBasePath
src="assets/img/icons/excel.svg"
alt="img"
/>
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderPrinterTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<i data-feather="printer" className="feather-printer" />
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderRefreshTooltip}>
<Link data-bs-toggle="tooltip" data-bs-placement="top">
<RotateCcw />
</Link>
</OverlayTrigger>
</li>
<li>
<OverlayTrigger placement="top" overlay={renderCollapseTooltip}>
<Link
data-bs-toggle="tooltip"
data-bs-placement="top"
id="collapse-header"
className={data ? "active" : ""}
onClick={() => {
dispatch(setToogleHeader(!data));
}}
>
<ChevronUp />
</Link>
</OverlayTrigger>
</li>
</ul>
<div className="page-btn">
<Link
to="#"
className="btn btn-added"
data-bs-toggle="modal"
data-bs-target="#add-category"
>
<PlusCircle className="me-2" />
Add New Category
</Link>
</div>
</div>
{/* /product list */}
<div className="card table-list-card">
<div className="card-body">
<div className="table-top">
<div className="search-set">
<div className="search-input">
<input
type="text"
placeholder="Search"
className="form-control form-control-sm formsearch"
onChange={handleSearch}
/>
<Link to className="btn btn-searchset">
<i data-feather="search" className="feather-search" />
</Link>
</div>
</div>
<Select
style={{ height: 36 }}
defaultValue={dateOptions[0]?.value}
options={dateOptions}
/>
</div>
<div className="table-responsive">
{loading ? (
<div className="text-center p-4">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="mt-2">Loading categories...</p>
</div>
) : error ? (
<div className="alert alert-danger" role="alert">
<strong>Error:</strong> {error}
<button
className="btn btn-sm btn-outline-danger ms-2"
onClick={() => dispatch(fetchPaymentMethods())}
>
Retry
</button>
</div>
) : (
<>
<Table columns={columns} dataSource={dataSource} />
<CustomPagination
currentPage={currentPage}
pageSize={pageSize}
totalCount={totalRecords}
totalPages={actualTotalPages}
loading={loading}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
pageSizeOptions={[10, 20, 50, 100]}
showInfo={true}
showPageSizeSelector={true}
compact={false}
className="paymentmethod-list-pagination"
/>
</>
)}
</div>
</div>
</div>
{/* /category list */}
</div>
</div>
<AddCategoryList />
<EditCategoryList />
</div>
);
};
export default CompanyList;

View File

@ -5,7 +5,6 @@ const ENDPOINTS = {
CATEGORIES: 'categories', CATEGORIES: 'categories',
CATEGORY_BY_ID: (id) => `categories/${id}`, CATEGORY_BY_ID: (id) => `categories/${id}`,
CATEGORY_PRODUCTS: (id) => `categories/${id}/products`, CATEGORY_PRODUCTS: (id) => `categories/${id}/products`,
SEARCH: 'categories/search',
}; };
// Categories API service // Categories API service
@ -65,19 +64,6 @@ export const categoriesApi = {
} }
}, },
// Search categories
searchCategories: async (query, params = {}) => {
try {
const response = await api.get(ENDPOINTS.SEARCH, {
params: { q: query, ...params }
});
return response.data;
} catch (error) {
console.error('Error searching categories:', error);
throw error;
}
},
// Get products by category // Get products by category
getCategoryProducts: async (id, params = {}) => { getCategoryProducts: async (id, params = {}) => {
try { try {

View File

@ -0,0 +1,91 @@
import api from './api';
const ENDPOINTS = {
ORGANIZATIONS: 'organizations',
ORGANIZATION_BY_ID: (id) => `organizations/${id}`,
};
export const organizationsApi = {
// Get all organizations
getAllOrganizations: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.ORGANIZATIONS, { params });
return response.data;
} catch (error) {
console.error('Error fetching organizations:', error);
throw error;
}
},
// Get organization by ID
getOrganizationById: async (id) => {
try {
const response = await api.get(ENDPOINTS.ORGANIZATION_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching organization ${id}:`, error);
throw error;
}
},
// Create organization
createOrganization: async (data) => {
try {
const response = await api.post(ENDPOINTS.ORGANIZATIONS, data);
return response.data;
} catch (error) {
console.error('Error creating organization:', error);
throw error;
}
},
// Update organization
updateOrganization: async (id, data) => {
try {
const response = await api.put(ENDPOINTS.ORGANIZATION_BY_ID(id), data);
return response.data;
} catch (error) {
console.error(`Error updating organization ${id}:`, error);
throw error;
}
},
// Delete organization
deleteOrganization: async (id) => {
try {
const response = await api.delete(ENDPOINTS.ORGANIZATION_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting organization ${id}:`, error);
throw error;
}
},
// Bulk update
bulkUpdate: async (organizations) => {
try {
const response = await api.put(`${ENDPOINTS.ORGANIZATIONS}/bulk`, {
organizations,
});
return response.data;
} catch (error) {
console.error('Error bulk updating organizations:', error);
throw error;
}
},
// Bulk delete
bulkDelete: async (ids) => {
try {
const response = await api.delete(`${ENDPOINTS.ORGANIZATIONS}/bulk`, {
data: { ids },
});
return response.data;
} catch (error) {
console.error('Error bulk deleting organizations:', error);
throw error;
}
},
};
export default organizationsApi;

View File

@ -0,0 +1,91 @@
import api from './api';
const ENDPOINTS = {
PAYMENT_METHODS: 'payment-methods',
PAYMENT_METHOD_BY_ID: (id) => `payment-methods/${id}`,
};
export const paymentMethodsApi = {
// Get all payment methods
getAll: async (params = {}) => {
try {
const response = await api.get(ENDPOINTS.PAYMENT_METHODS, { params });
return response.data;
} catch (error) {
console.error('Error fetching payment methods:', error);
throw error;
}
},
// Get payment method by ID
getById: async (id) => {
try {
const response = await api.get(ENDPOINTS.PAYMENT_METHOD_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error fetching payment method ${id}:`, error);
throw error;
}
},
// Create new payment method
create: async (data) => {
try {
const response = await api.post(ENDPOINTS.PAYMENT_METHODS, data);
return response.data;
} catch (error) {
console.error('Error creating payment method:', error);
throw error;
}
},
// Update payment method
update: async (id, data) => {
try {
const response = await api.put(ENDPOINTS.PAYMENT_METHOD_BY_ID(id), data);
return response.data;
} catch (error) {
console.error(`Error updating payment method ${id}:`, error);
throw error;
}
},
// Delete payment method
remove: async (id) => {
try {
const response = await api.delete(ENDPOINTS.PAYMENT_METHOD_BY_ID(id));
return response.data;
} catch (error) {
console.error(`Error deleting payment method ${id}:`, error);
throw error;
}
},
// Bulk update
bulkUpdate: async (methods) => {
try {
const response = await api.put(`${ENDPOINTS.PAYMENT_METHODS}/bulk`, {
methods,
});
return response.data;
} catch (error) {
console.error('Error bulk updating payment methods:', error);
throw error;
}
},
// Bulk delete
bulkDelete: async (ids) => {
try {
const response = await api.delete(`${ENDPOINTS.PAYMENT_METHODS}/bulk`, {
data: { ids },
});
return response.data;
} catch (error) {
console.error('Error bulk deleting payment methods:', error);
throw error;
}
},
};
export default paymentMethodsApi;

View File

@ -10,4 +10,8 @@ const formatDate = (isoDate) => {
return formatted return formatted
}; };
export { formatDate }; const formatInputDate = (date) => {
return new Date(date).toLocaleDateString("en-CA");
};
export { formatDate, formatInputDate };