-
+
-
-
-
-
+ {!isAdminPageLayoutShown && (
+
+ <>
+
+
+ >
+
+ )}
+ {isAdminPageLayoutShown && (
+ <>
+ {isLogoutModalShown && (
+
+ )}
+
+ >
+ )}
);
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/container/OrdersTableContainer.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/container/OrdersTableContainer.tsx
new file mode 100644
index 00000000..ac5bfefe
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/container/OrdersTableContainer.tsx
@@ -0,0 +1,106 @@
+import { useCallback, useEffect, useState } from "react";
+import { DATE_TIME_FORMATS } from "../../../../constants";
+import { ORDER_STATUS } from "../../../../data/applicationData";
+import OrderService from "../../../../services/order/OrderService";
+import { OrderClass } from "../../../../services/order/OrderTypes";
+import {
+ convertUTCToLocalTime,
+ formatDateTime,
+} from "../../../../utils/dateTimeHelper";
+import OrdersTable from "../presentation/OrdersTable";
+
+const OrdersTableContainer = () => {
+
+ /*
+ Flag for whether orders are being fetched
+ (To show loading spinner until the first response come)
+ */
+ const [isFetchingOrders, setIsFetchingOrders] = useState(true);
+
+ /* Key value orders object, key is id and value is the order */
+ const [orders, setOrders] = useState<{ [key: string]: OrderClass }>({});
+
+ /* To know if an error has occurred when fetching orders */
+ const [isError, setIsError] = useState(false);
+
+ const formatOrder = (order: OrderClass) => {
+ /* Cloning the object, so the original object doesn't get updated */
+ order = { ...order };
+
+ /* Converting createdAt and updatedAt to local and formatting them */
+ order.createdAt = formatDateTime(
+ convertUTCToLocalTime(
+ order.createdAt,
+ DATE_TIME_FORMATS.standardDateWithTime
+ ),
+ DATE_TIME_FORMATS.standardDateWithTime,
+ DATE_TIME_FORMATS.displayedDateWithTime
+ );
+ order.updatedAt = formatDateTime(
+ convertUTCToLocalTime(
+ order.updatedAt,
+ DATE_TIME_FORMATS.standardDateWithTime
+ ),
+ DATE_TIME_FORMATS.standardDateWithTime,
+ DATE_TIME_FORMATS.displayedDateWithTime
+ );
+
+ return order;
+ };
+
+ /* Fetch Orders Asynchronously */
+ const fetchOrders = useCallback((status: ORDER_STATUS) => {
+ setOrders({});
+ setIsFetchingOrders(true);
+ OrderService.getOrdersAsync(status, (data, _, error) => {
+ if (!error) {
+ setOrders((prev) => {
+ data.map((order) => {
+ /* Formatting order */
+ order = formatOrder(order);
+
+ /* At key: Order ID, value is the order object */
+ prev[order._id] = order;
+ });
+ return { ...prev };
+ });
+ setIsFetchingOrders(false);
+ } else {
+ console.error("Error -- fetchOrders() Admin", error);
+ setIsFetchingOrders(false);
+ setIsError(true);
+ }
+ });
+ }, []);
+
+
+ /* Once a order status has been updated */
+ const onOrderStatusUpdatedHandler = (
+ orderId: string,
+ _: ORDER_STATUS
+ ) => {
+ /* Update order object at the orderId */
+ setOrders((prev) => {
+ delete prev[orderId]
+ return { ...prev };
+ });
+ };
+
+ /* Initial Render */
+ useEffect(() => {
+ fetchOrders(ORDER_STATUS.PENDING);
+ }, [fetchOrders]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default OrdersTableContainer;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrderLinkCell.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrderLinkCell.tsx
new file mode 100644
index 00000000..8543f51d
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrderLinkCell.tsx
@@ -0,0 +1,30 @@
+import { CustomCellRendererProps } from "ag-grid-react";
+import Link from "../../../basic/Link";
+import { LinkTypes, QUERY_PARAMS, ROUTE_PATHS } from "../../../../constants";
+import useCustomNavigate from "../../../../hooks/useCustomNavigate";
+import { createSearchParams } from "react-router-dom";
+
+const OrderLinkCell = (props: CustomCellRendererProps) => {
+ const navigate = useCustomNavigate();
+
+ /* Navigate to the order detail page of the particular order */
+ const orderIdClickHandler = () => {
+ navigate({
+ pathname: ROUTE_PATHS.orderDetail,
+ search: createSearchParams({
+ [QUERY_PARAMS.orderId]: props.value,
+ }).toString(),
+ });
+ };
+ return (
+ <>
+
+ >
+ );
+};
+
+export default OrderLinkCell;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrdersOptionsCell.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrdersOptionsCell.tsx
new file mode 100644
index 00000000..95d4a9e9
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrdersOptionsCell.tsx
@@ -0,0 +1,26 @@
+import { CustomCellRendererProps } from "ag-grid-react";
+import { OrderClass } from "../../../../services/order/OrderTypes";
+import Button from "../../../basic/Button";
+import EditIcon from "../../../icons/EditIcon";
+
+interface OrdersOptionsCellProps extends CustomCellRendererProps {
+ onEditClickHandler(order: OrderClass): void;
+}
+/* For options cell for a particular order */
+const OrdersOptionsCell = (props: OrdersOptionsCellProps) => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default OrdersOptionsCell;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrdersTable.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrdersTable.tsx
new file mode 100644
index 00000000..d4aa1b8f
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/orderstable/presentation/OrdersTable.tsx
@@ -0,0 +1,216 @@
+import { ColDef } from "ag-grid-community";
+import { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ RADIO_BUTTON_TYPE
+} from "../../../../constants";
+import { ORDER_STATUS } from "../../../../data/applicationData";
+import { OrderClass } from "../../../../services/order/OrderTypes";
+import { useAppSelector } from "../../../../store";
+import {
+ gridDateFilterComparator,
+ gridDateSortComparator,
+} from "../../../../utils/dateTimeHelper";
+import ErrorMessage from "../../../basic/ErrorMessage";
+import Grid from "../../../basic/Grid";
+import RadioButtons from "../../../basic/RadioButtons";
+import EditOrderStatusModalContainer from "../../../modals/editorderstatusmodal/container/EditOrderStatusModalContainer";
+import OrderLinkCell from "./OrderLinkCell";
+import OrdersOptionsCell from "./OrdersOptionsCell";
+
+interface OrdersTableProps {
+ orders: OrderClass[] | null;
+ isError: boolean;
+ onOrderStatusUpdatedHandler(orderId: string, status: ORDER_STATUS): void;
+ fetchOrders(status: ORDER_STATUS): void;
+}
+const OrdersTable = (props: OrdersTableProps) => {
+ const { orders, onOrderStatusUpdatedHandler, fetchOrders, isError } = props;
+
+ const { t } = useTranslation();
+
+ const isRTL = useAppSelector((state) => state.language.isRTL);
+
+ /* Visibility of Edit Order dialog */
+ const [isEditOrderModalShown, setIsEditOrderModalShown] = useState(false);
+
+ /* Order status of the orders which are visibile */
+ const [selectedOrderStatus, setSelectedOrderStatus] = useState(
+ ORDER_STATUS.PENDING
+ );
+
+ /* Selected order for edit operation*/
+ const [selectedOrder, setSelectedOrder] = useState
();
+
+ /* Column Defination for the grid */
+ const ORDERS_TABLE_COL_DEFS: ColDef[] = [
+ {
+ field: "_id",
+ sortable: false,
+ filter: "agTextColumnFilter",
+ cellRenderer: OrderLinkCell,
+ filterParams: {
+ maxNumConditions: 1,
+ filterOptions: ["contains"],
+ },
+ },
+ {
+ field: "customer.username",
+ sortable: false,
+ filter: "agTextColumnFilter",
+ filterParams: {
+ maxNumConditions: 1,
+ filterOptions: ["contains"],
+ },
+ },
+ {
+ field: "customer.email",
+ sortable: false,
+ filter: "agTextColumnFilter",
+ filterParams: {
+ maxNumConditions: 1,
+ filterOptions: ["contains"],
+ },
+ },
+ {
+ field: "createdAt",
+ unSortIcon: true,
+ comparator: gridDateSortComparator,
+ filter: "agDateColumnFilter",
+ filterParams: {
+ suppressAndOrCondition: true,
+ filterOptions: ["equals"],
+ comparator: gridDateFilterComparator,
+ },
+ },
+ {
+ field: "updatedAt",
+ unSortIcon: true,
+ comparator: gridDateSortComparator,
+ filter: "agDateColumnFilter",
+ filterParams: {
+ suppressAndOrCondition: true,
+ filterOptions: ["equals"],
+ comparator: gridDateFilterComparator,
+ },
+ },
+ {
+ field: "discountedOrderPrice",
+ sortable: true,
+ },
+ {
+ field: "paymentId",
+ sortable: false,
+ filter: "agTextColumnFilter",
+ filterParams: {
+ maxNumConditions: 1,
+ filterOptions: ["contains"],
+ },
+ },
+ {
+ field: "",
+ maxWidth: 100,
+ resizable: false,
+ hide: selectedOrderStatus !== ORDER_STATUS.PENDING,
+ cellRenderer: OrdersOptionsCell,
+ cellRendererParams: {
+ onEditClickHandler: toggleEditOrderModal,
+ },
+ pinned: isRTL ? "left" : "right",
+ },
+ ];
+
+ const orderStatusRadioButtons: Array> =
+ useMemo(() => {
+ return [
+ {
+ label: t("pending"),
+ isDefaultSelected: true,
+ data: ORDER_STATUS.PENDING,
+ id: ORDER_STATUS.PENDING,
+ },
+ {
+ label: t("cancelled"),
+ isDefaultSelected: false,
+ data: ORDER_STATUS.CANCELLED,
+ id: ORDER_STATUS.CANCELLED,
+ },
+ {
+ label: t("delivered"),
+ isDefaultSelected: false,
+ data: ORDER_STATUS.DELIVERED,
+ id: ORDER_STATUS.DELIVERED,
+ },
+ ];
+ }, [t]);
+
+ /* On order type changed */
+ const onOrderTypeChangeHandler = (status: ORDER_STATUS) => {
+ /* If the previous status is not equal to the new status */
+ if (status !== selectedOrderStatus) {
+ /* Set selected order status */
+ setSelectedOrderStatus(status);
+
+ /* Fetch orders of the particular status */
+ fetchOrders(status);
+ }
+ };
+
+ /* Toggle Edit Order Dialog */
+ function toggleEditOrderModal(order: OrderClass) {
+ /* If there is no selected order: Toggle is to show the dialog */
+ if (!selectedOrder && order) {
+ /* Set selected order */
+ setSelectedOrder(order);
+ } else {
+ /* Set order to undefined */
+ setSelectedOrder(undefined);
+ }
+ /* Toggle the dialog */
+ setIsEditOrderModalShown((prev) => !prev);
+ }
+
+ return (
+ <>
+ {isError ? (
+
+ ) : (
+ <>
+ {isEditOrderModalShown && selectedOrder && (
+ toggleEditOrderModal(selectedOrder)}
+ order={selectedOrder}
+ onOrderStatusUpdatedHandler={onOrderStatusUpdatedHandler}
+ />
+ )}
+
+
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default OrdersTable;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/container/ProductsTableContainer.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/container/ProductsTableContainer.tsx
new file mode 100644
index 00000000..0cabd14f
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/container/ProductsTableContainer.tsx
@@ -0,0 +1,130 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { Product } from "../../../../services/product/ProductTypes";
+import ProductService from "../../../../services/product/ProductService";
+import ProductsTable from "../presentation/ProductsTable";
+import CategoryService from "../../../../services/category/CategoryService";
+import ApiError from "../../../../services/ApiError";
+
+export interface ProductWithCategoryName extends Product {
+ categoryName: string;
+}
+const ProductsTableContainer = () => {
+
+
+ /*
+ Flag for whether products are being fetched
+ (To show loading spinner until the first response come)
+ */
+ const [isFetchingProducts, setIsFetchingProducts] = useState(true);
+
+ /* Key value products object, key is product id and value is the Product */
+ const [products, setProducts] = useState<{
+ [key: string]: ProductWithCategoryName;
+ }>({});
+
+ /* To store category names as object, id is categoryId and value is categoryName */
+ const categoryNames = useRef<{ [key: string]: string }>({});
+
+ /* To know if an error has occurred when fetching products */
+ const [isError, setIsError] = useState(false);
+
+ /* Get category name by categoryId */
+ const getCategoryName = async (categoryId: string): Promise => {
+ /* Checking for category name locally */
+ const categoryName = categoryNames.current?.[categoryId];
+
+ /* If the category name is not found */
+ if (!categoryName) {
+ /* Fetch category by id */
+ const response = await CategoryService.getCategoryById(categoryId);
+
+ /* Success return category name */
+ if (!(response instanceof ApiError)) {
+ /* Save it in memory */
+ categoryNames.current[categoryId] = response.name;
+
+ return response.name;
+ } else {
+ /* Return empty string */
+ return "";
+ }
+ }
+ return categoryName;
+ };
+
+ const fetchProducts = useCallback(async () => {
+
+ setIsFetchingProducts(true);
+ /* Hiding error, Displaying loading spinner, Resetting products list state */
+ setIsError(false);
+
+ /* Get all products asynchronously */
+ ProductService.getAllProductsAsync(async (data, _, error) => {
+ /* Success */
+ if (!error) {
+ const tempProducts: typeof products = {};
+
+ /* Iterating through fetched products */
+ for (const product of data) {
+ /* Category Name*/
+ const categoryName = await getCategoryName(product.category)
+
+ /* Set products state */
+ tempProducts[product._id] = { ...product, categoryName };
+ }
+
+ /* Set products */
+ setProducts((prev) => {
+ return { ...prev, ...tempProducts };
+ });
+ setIsFetchingProducts(false);
+ } else {
+
+ setIsFetchingProducts(false);
+ /* API Error */
+ setIsError(true);
+ }
+ });
+ }, []);
+
+ /* Once a product has been updated or added (In order to avoid another apiCall) */
+ const onProductAddedOrUpdatedHandler = async (newProduct: Product) => {
+ /* Getting the category name */
+ const categoryName = await getCategoryName(newProduct.category);
+
+ /* Update products object at the productId */
+ setProducts((prev) => {
+ prev[newProduct._id] = {
+ ...newProduct,
+ categoryName
+ };
+
+ return { ...prev };
+ });
+ };
+
+ const onProductDeletedHandler = (deletedProduct: Product) => {
+ /* Removing the key value pair of the deletedProduct */
+ setProducts((prev) => {
+ delete prev[deletedProduct._id];
+ return { ...prev };
+ });
+ };
+
+ useEffect(() => {
+ fetchProducts();
+ }, [fetchProducts]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default ProductsTableContainer;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/presentation/ProductOptionsCell.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/presentation/ProductOptionsCell.tsx
new file mode 100644
index 00000000..83d36f19
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/presentation/ProductOptionsCell.tsx
@@ -0,0 +1,34 @@
+import { CustomCellRendererProps } from "ag-grid-react";
+import { Product } from "../../../../services/product/ProductTypes";
+import Button from "../../../basic/Button";
+import DeleteIcon from "../../../icons/DeleteIcon";
+import EditIcon from "../../../icons/EditIcon";
+
+interface ProductOptionsCellProps extends CustomCellRendererProps {
+ onEditOrDeleteClickHandler(product: Product, type: "DELETE" | "EDIT"): void;
+}
+/* For options cell for a particular product in the Products Table */
+const ProductOptionsCell = (props: ProductOptionsCellProps) => {
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default ProductOptionsCell;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/presentation/ProductsTable.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/presentation/ProductsTable.tsx
new file mode 100644
index 00000000..929f8f94
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/components/widgets/productstable/presentation/ProductsTable.tsx
@@ -0,0 +1,194 @@
+import {
+ ColDef,
+ SizeColumnsToContentStrategy,
+ SizeColumnsToFitGridStrategy,
+ SizeColumnsToFitProvidedWidthStrategy,
+} from "ag-grid-community";
+import { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { BREAKPOINTS, ButtonTypes } from "../../../../constants";
+import useBreakpointCheck from "../../../../hooks/useBreakpointCheck";
+import { Product } from "../../../../services/product/ProductTypes";
+import { useAppSelector } from "../../../../store";
+import Button from "../../../basic/Button";
+import ErrorMessage from "../../../basic/ErrorMessage";
+import Grid from "../../../basic/Grid";
+import { ProductWithCategoryName } from "../container/ProductsTableContainer";
+import ProductOptionsCell from "./ProductOptionsCell";
+import AddEditProductModalContainer from "../../../modals/addeditproductmodal/container/AddEditProductModalContainer";
+import DeleteProductModalContainer from "../../../modals/deleteproductmodal/container/DeleteProductModalContainer";
+
+interface ProductsTableProps {
+ products: ProductWithCategoryName[] | null;
+ isError: boolean;
+ onProductAddedOrUpdated(newProduct: Product): void;
+ onProductDeletedHandler(deletedProduct: Product): void;
+}
+const ProductsTable = (props: ProductsTableProps) => {
+ const {
+ products,
+ onProductAddedOrUpdated,
+ onProductDeletedHandler,
+ isError,
+ } = props;
+
+ const { t } = useTranslation();
+
+ const isRTL = useAppSelector((state) => state.language.isRTL);
+
+
+ /* Is large screen */
+ const isLG = useBreakpointCheck(BREAKPOINTS.lg);
+
+ /* Column Defination for the grid */
+ const PRODUCTS_TABLE_COL_DEFS: ColDef[] = [
+ {
+ field: "name",
+ sortable: false,
+ filter: "agTextColumnFilter",
+ filterParams: {
+ maxNumConditions: 1,
+ filterOptions: ["contains"],
+ },
+ },
+ {
+ field: "categoryName",
+ sortable: true,
+ filter: "agTextColumnFilter",
+ filterParams: {
+ maxNumConditions: 1,
+ filterOptions: ["contains"],
+ },
+ },
+ {
+ field: "description",
+ sortable: false,
+ tooltipField: "description",
+ },
+ {
+ field: "price",
+ sortable: true,
+ },
+ {
+ field: "stock",
+ sortable: true,
+ },
+ {
+ field: "",
+ maxWidth: 100,
+ resizable: false,
+ cellRenderer: ProductOptionsCell,
+ cellRendererParams: {
+ onEditOrDeleteClickHandler: toggleEditOrDeleteProductModal,
+ },
+ pinned: !isLG ? (isRTL ? 'left' : 'right') : false
+ },
+ ];
+
+
+ /* Visibility of AddEdit Product dialog */
+ const [isAddEditProdutModalShown, setIsAddEditProductModalShown] =
+ useState(false);
+
+ /* Visibility of Delete Product dialog */
+ const [isDeleteProductModalShown, setIsDeleteProductModalShown] =
+ useState(false);
+
+ /* Selected product for edit & delete options */
+ const [selectedProduct, setSelectedProduct] = useState();
+
+ /* Toggle Add Product Dialog */
+ const toggleAddProductModal = () => {
+ /* Reset selected product */
+ setSelectedProduct(undefined);
+ setIsAddEditProductModalShown((prev) => !prev);
+ };
+
+ /* Toggle Edit or Delete Product Dialog */
+ function toggleEditOrDeleteProductModal(
+ product: Product,
+ type: "EDIT" | "DELETE"
+ ) {
+ /* If there is no product: Toggle is to show the dialog */
+ if (!selectedProduct && product) {
+ /* Set selected product */
+ setSelectedProduct(product);
+ } else {
+ /* Set product to undefined */
+ setSelectedProduct(undefined);
+ }
+ /* Toggle the dialog */
+ if (type === "EDIT") {
+ setIsAddEditProductModalShown((prev) => !prev);
+ } else {
+ setIsDeleteProductModalShown((prev) => !prev);
+ }
+ }
+
+ /*
+ On Large screens fit the entire width, on smaller screens fit the contents of a cell
+ */
+ const autoSizeStrategy:
+ | SizeColumnsToFitGridStrategy
+ | SizeColumnsToFitProvidedWidthStrategy
+ | SizeColumnsToContentStrategy = useMemo(() => {
+ if (isLG) {
+ return {
+ type: "fitGridWidth",
+ };
+ }
+ return {
+ type: "fitCellContents",
+ };
+ }, [isLG]);
+
+ return (
+ <>
+ {isError ? (
+
+ ) : (
+ <>
+ {isAddEditProdutModalShown && (
+
+ )}
+ {isDeleteProductModalShown && selectedProduct && (
+
+ toggleEditOrDeleteProductModal(selectedProduct, "DELETE")
+ }
+ onProductDeleted={onProductDeletedHandler}
+ product={selectedProduct}
+ />
+ )}
+
+
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default ProductsTable;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/constants.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/constants.ts
index 1a766d19..db71c71e 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/constants.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/constants.ts
@@ -1,6 +1,8 @@
import React from "react";
import { AddressClass } from "./services/address/AddressTypes";
import { DateRange } from "react-day-picker";
+import { Category } from "./services/category/CategoryTypes";
+import { COUPON_TYPES } from "./services/coupon/CouponTypes";
/* DROPDOWN */
export enum DropdownTypes {
@@ -139,7 +141,17 @@ export enum ROUTE_PATHS {
orderDetail = "/order",
resetForgottenPassword = "/forgot-password/:token",
pageNotFound = "/page-not-found",
- about = "/about"
+ about = "/about",
+ admin = "/admin",
+ adminCategories = "/admin/categories",
+ adminProducts = "/admin/products",
+ adminOrders = "/admin/orders",
+ adminCoupons = "/admin/coupons"
+}
+
+export enum USER_ROLES {
+ admin = "ADMIN",
+ user = "USER"
}
@@ -269,4 +281,57 @@ export interface ProductFilterFields {
export interface OrderListFilterFields {
dateRange: DateRange,
checkedStatus: Array>
-}
\ No newline at end of file
+}
+
+export interface AddCategoryFields {
+ categoryName: string;
+}
+
+export interface EditCategoryFields {
+ category: Category;
+ newCategoryName: string;
+}
+
+export interface AddEditProductFields {
+ name: string,
+ description: string,
+ category: DropdownItem,
+ price: number,
+ stock: number,
+ mainImage: File,
+ subImage1: File,
+ subImage2: File,
+ subImage3: File,
+ subImage4: File
+}
+
+export interface AddEditProductFieldsForService {
+ name: string,
+ description: string,
+ category: DropdownItem,
+ price: number,
+ stock: number,
+ mainImage: File,
+ subImages: File[]
+}
+
+/* For api call */
+export interface EditProductFieldsForService {
+ name?: string,
+ description?: string,
+ category?: DropdownItem,
+ price?: number,
+ stock?: number,
+ mainImage?: File,
+ subImages?: File[]
+}
+
+export interface AddEditCouponFields {
+ name: string,
+ couponCode: string,
+ type: COUPON_TYPES,
+ discountValue: number,
+ minimumCartValue: number,
+ startDate: Date,
+ expiryDate: Date
+}
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/data/applicationData.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/data/applicationData.tsx
index a59bf600..61a6b362 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/data/applicationData.tsx
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/data/applicationData.tsx
@@ -11,8 +11,10 @@ import {
CategoryIcon,
DropdownItem,
NavigationOption,
+ ROUTE_PATHS,
SelectionMenuItem,
TabItemConfig,
+ USER_ROLES,
} from "../constants";
import i18n from "../i18n";
@@ -25,40 +27,72 @@ export const DRAWER_ITEMS: Array = [
{
id: 2,
textKey: "about",
- navigateTo: "/about",
+ navigateTo: ROUTE_PATHS.about,
},
{
id: 3,
textKey: "login",
- navigateTo: "/login",
+ navigateTo: ROUTE_PATHS.login,
},
];
-export const getNavigationItemList = (isLoggedIn: boolean) => {
+export const ADMIN_NAVIGATION_ITEMS: Array = [
+ {
+ id: 1,
+ textKey: "categories",
+ navigateTo: ROUTE_PATHS.adminCategories,
+ },
+ {
+ id: 2,
+ textKey: "products",
+ navigateTo: ROUTE_PATHS.adminProducts,
+ },
+ {
+ id: 3,
+ textKey: "orders",
+ navigateTo: ROUTE_PATHS.adminOrders,
+ },
+ {
+ id: 4,
+ textKey: "coupons",
+ navigateTo: ROUTE_PATHS.adminCoupons,
+ }
+];
+
+export const getNavigationItemList = (
+ isLoggedIn: boolean,
+ role: USER_ROLES
+) => {
const tempDrawerItems = [...DRAWER_ITEMS];
if (isLoggedIn) {
tempDrawerItems.pop();
+ if (role === USER_ROLES.admin) {
+ tempDrawerItems.push({
+ id: 4,
+ textKey: "admin",
+ navigateTo: ROUTE_PATHS.adminCategories,
+ });
+ }
tempDrawerItems.push({
- id: 4,
+ id: 5,
textKey: "myAccount",
navigateTo: "my-account",
- customComponent:
+ customComponent: ,
});
}
return tempDrawerItems;
};
-
export const MY_ACCOUNT_OPTIONS: Array = [
{
id: 1,
textKey: "manageAccount",
- icon: ,
+ icon: ,
},
{
id: 2,
textKey: "myOrders",
- icon:
+ icon: ,
},
{
id: 3,
@@ -75,7 +109,12 @@ export const DEFAULT_CURRENCY = "INR";
export const DEFAULT_COUNTRY = "United Arab Emirates";
-export const COUNTRIES_DROPDOWN_LIST: DropdownItem[] = [{id: 1, text: DEFAULT_COUNTRY}, {id: 2, text: "India"}]
+export const COUPON_CODE_MINIMUM_LENGTH = 4;
+
+export const COUNTRIES_DROPDOWN_LIST: DropdownItem[] = [
+ { id: 1, text: DEFAULT_COUNTRY },
+ { id: 2, text: "India" },
+];
export const COMPANY_GURANTEE_LIST: COMPANY_GURANTEE[] = [
{
id: 1,
@@ -100,47 +139,50 @@ export const COMPANY_GURANTEE_LIST: COMPANY_GURANTEE[] = [
export const EXPLORE_PRODUCTS_COUNT = 8;
export const FEATURED_PRODUCTS_COUNT = 4;
export const RELATED_PRODUCTS_COUNT = 4;
+export const MAX_SUBIMAGES_PER_PRODUCT = 4;
export enum PAYMENT_TYPES {
- PAYPAL = "PAYPAL"
+ PAYPAL = "PAYPAL",
}
export const MANAGE_ACCOUNT_TABS: Array = [
{
- id: 1,
- tabHeadingKey: "editProfile"
+ id: 1,
+ tabHeadingKey: "editProfile",
},
{
- id: 2,
- tabHeadingKey: "myAddresses"
- }
-]
+ id: 2,
+ tabHeadingKey: "myAddresses",
+ },
+];
export enum ORDER_STATUS {
- PENDING= "PENDING",
- CANCELLED= "CANCELLED",
- DELIVERED= "DELIVERED",
+ PENDING = "PENDING",
+ CANCELLED = "CANCELLED",
+ DELIVERED = "DELIVERED",
}
export const ORDER_STATUS_FILTERS_CHECKBOX: Array> = [
{
id: ORDER_STATUS.PENDING,
- data: null,
+ data: null,
isDefaultSelected: true,
isLabelKey: true,
- label: i18n.t("pending")
+ label: i18n.t("pending"),
},
{
id: ORDER_STATUS.CANCELLED,
- data: null,
+ data: null,
isDefaultSelected: true,
label: i18n.t("cancelled"),
isLabelKey: true,
},
{
id: ORDER_STATUS.DELIVERED,
- data: null,
+ data: null,
isDefaultSelected: true,
label: i18n.t("delivered"),
isLabelKey: true,
- }
-]
+ },
+];
+
+
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/layouts/PageLayout.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/layouts/PageLayout.tsx
index 874470ab..a60aae95 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/layouts/PageLayout.tsx
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/layouts/PageLayout.tsx
@@ -3,20 +3,35 @@ import { useDispatch } from "react-redux";
import { updateBreakpoint } from "../store/BreakpointSlice";
import { getCurrentBreakpoint } from "../utils/breakpointsHelper";
-import { Outlet } from "react-router-dom";
+import { Outlet, useLocation } from "react-router-dom";
import ArrowButton from "../components/basic/ArrowButton";
import ToastMessage from "../components/basic/ToastMessage";
import FooterContainer from "../components/widgets/footer/container/FooterContainer";
import HeaderContainer from "../components/widgets/header/container/HeaderContainer";
-import { ARROW_BUTTONS } from "../constants";
+import { ARROW_BUTTONS, ROUTE_PATHS, USER_ROLES } from "../constants";
import { useAppSelector } from "../store";
+import { updateHeaderHeight } from "../store/UIinfoSlice";
const PageLayout = () => {
const dispatch = useDispatch();
+ /* Page Location */
+ const location = useLocation();
+
/* isRTL language */
const isRTL = useAppSelector((state) => state.language.isRTL);
+ /* Logged in user details */
+ const userDetails = useAppSelector((state) => state.auth.userDetails);
+
+ /*
+ Shown admin page layout: if the logged in user is admin,
+ and the location path includes is /admin
+ */
+ const isAdminPageLayoutShown =
+ userDetails?.role === USER_ROLES.admin &&
+ location.pathname.includes(ROUTE_PATHS.admin);
+
/* Header height in pixels */
const [headerHeight, setHeaderHeight] = useState("0px");
@@ -44,7 +59,8 @@ const PageLayout = () => {
/* Updating header height */
useEffect(() => {
setHeaderHeight(`${headerContainerRef.current?.clientHeight}px`);
- }, [headerContainerRef]);
+ dispatch(updateHeaderHeight(headerContainerRef?.current?.clientHeight));
+ }, [headerContainerRef, dispatch]);
/* Scroll to top smoothly */
const scrollToTop = () => {
@@ -55,20 +71,22 @@ const PageLayout = () => {
-
+
-
+ {!isAdminPageLayoutShown && (
+
+ )}
);
};
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/pages/admin/container/AdminPageContainer.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/pages/admin/container/AdminPageContainer.tsx
new file mode 100644
index 00000000..16cc67a9
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/pages/admin/container/AdminPageContainer.tsx
@@ -0,0 +1,12 @@
+import AdminCategoriesPage from "../presentation/AdminPage";
+
+
+const AdminPageContainer = () => {
+ return (
+ <>
+
+ >
+ )
+}
+
+export default AdminPageContainer;
\ No newline at end of file
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/pages/admin/presentation/AdminPage.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/pages/admin/presentation/AdminPage.tsx
new file mode 100644
index 00000000..445cb9a6
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/pages/admin/presentation/AdminPage.tsx
@@ -0,0 +1,37 @@
+import { useMemo } from "react";
+import CategoriesTableContainer from "../../../components/widgets/categoriestable/container/CategoriesTableContainer";
+import { useAppSelector } from "../../../store";
+import { useLocation } from "react-router-dom";
+import { ROUTE_PATHS } from "../../../constants";
+import ProductsTableContainer from "../../../components/widgets/productstable/container/ProductsTableContainer";
+import CouponsTableContainer from "../../../components/widgets/couponstable/container/CouponsTableContainer";
+import OrdersTableContainer from "../../../components/widgets/orderstable/container/OrdersTableContainer";
+
+const AdminPage = () => {
+ const location = useLocation();
+
+ const headerHeight = useAppSelector((state) => state.uiInfo.headerHeight);
+
+ /* Setting height as total screen height - headerHeight, for AG grid */
+ const pageContentHeight = useMemo(() => {
+ return `${window.innerHeight - headerHeight}px`;
+ }, [headerHeight]);
+
+ return (
+
+ {location.pathname === ROUTE_PATHS.adminCategories ? (
+
+ ) : location.pathname === ROUTE_PATHS.adminProducts ? (
+
+ ) : location.pathname === ROUTE_PATHS.adminCoupons ? (
+
+ ) : location.pathname === ROUTE_PATHS.adminOrders ? (
+
+ ) : (
+ <>>
+ )}
+
+ );
+};
+
+export default AdminPage;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/protectedroutes/ForAdminUsers.tsx b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/protectedroutes/ForAdminUsers.tsx
new file mode 100644
index 00000000..26e7c11c
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/protectedroutes/ForAdminUsers.tsx
@@ -0,0 +1,27 @@
+import { Outlet } from "react-router-dom";
+import { USER_ROLES } from "../services/auth/AuthTypes";
+import { useAppSelector } from "../store";
+import { useEffect } from "react";
+import useCustomNavigate from "../hooks/useCustomNavigate";
+
+
+const ForAdminUsers = () => {
+ const user = useAppSelector((state) => state.auth.userDetails);
+ const isLogInCheckDone = useAppSelector((state) => state.auth.isLogInCheckDone);
+
+ const navigate = useCustomNavigate();
+
+ useEffect(() => {
+ if(isLogInCheckDone && user?.role !== USER_ROLES.admin){
+ navigate("/", true);
+ }
+ }, [isLogInCheckDone, user?.role, navigate])
+
+ if(isLogInCheckDone && user && user.role == USER_ROLES.admin){
+ return ;
+ }
+
+ return <>>
+}
+
+export default ForAdminUsers;
\ No newline at end of file
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/UtilServices.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/UtilServices.ts
new file mode 100644
index 00000000..01a57d29
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/UtilServices.ts
@@ -0,0 +1,19 @@
+import axios from "axios";
+
+
+class UtilServices {
+
+ private axiosInstance = axios.create({withCredentials: false});
+
+ async getImageUrlAsFileObject(url: string, fileName: string){
+ try{
+ const response = await this.axiosInstance.get(url, {responseType: 'blob'});
+ return new File([response.data], fileName);
+ }
+ catch(error){
+ return new File([], '');
+ }
+ }
+}
+
+export default new UtilServices();
\ No newline at end of file
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/category/CategoryService.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/category/CategoryService.ts
index 8f1ed60d..2b49dad3 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/category/CategoryService.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/category/CategoryService.ts
@@ -1,3 +1,4 @@
+import { EditCategoryFields } from "../../constants";
import ApiError from "../ApiError";
import ApiRequest from "../ApiRequest";
import ApiResponse from "../ApiResponse";
@@ -10,7 +11,11 @@ class CategoryService {
/* Get All Categories Asynchronously: As the requests keep fulfilling response will be sent as callback */
async getAllCategoriesAsync(
- callback: (data: Category[], isDone: boolean, errorMessage?: ApiError) => void
+ callback: (
+ data: Category[],
+ isDone: boolean,
+ errorMessage?: ApiError
+ ) => void
) {
/* API Request */
const apiRequest = new ApiRequest(this.BASE_URL);
@@ -74,6 +79,68 @@ class CategoryService {
: callback([], true, new ApiError(firstResponse.message));
}
}
+
+ async addCategory(categoryName: string): Promise {
+ const apiRequest = new ApiRequest(this.BASE_URL);
+
+ const response = await apiRequest.postRequest({
+ name: categoryName,
+ });
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async editCategory(fields: EditCategoryFields): Promise {
+ const apiRequest = new ApiRequest(
+ `${this.BASE_URL}/${fields.category._id}`
+ );
+
+ const response = await apiRequest.patchRequest({
+ name: fields.newCategoryName,
+ });
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async deleteCategory(
+ category: Category
+ ): Promise | ApiError> {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/${category._id}`);
+
+ const response = await apiRequest.deleteRequest<{
+ deletedCategory: Category;
+ }>();
+
+ if (response instanceof ApiResponse && response.success) {
+ return response;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async getCategoryById(categoryId: string): Promise {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/${categoryId}`);
+
+ const response = await apiRequest.getRequest();
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
}
export default new CategoryService();
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/coupon/CouponService.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/coupon/CouponService.ts
index e2f2d2c6..45fab486 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/coupon/CouponService.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/coupon/CouponService.ts
@@ -1,14 +1,87 @@
+import { AddEditCouponFields, DATE_TIME_FORMATS } from "../../constants";
+import { getMomentObjectFromDateObject } from "../../utils/dateTimeHelper";
import ApiError from "../ApiError";
import ApiRequest from "../ApiRequest";
import ApiResponse from "../ApiResponse";
import { UserCart } from "../cart/CartTypes";
-import { CouponClass, CouponListClass } from "./CouponTypes";
+import { COUPON_TYPES, CouponClass, CouponListClass } from "./CouponTypes";
class CouponService {
BASE_URL = "/api/v1/ecommerce/coupons";
defaultPageLimit = 50;
defaultPageNumber = 1;
+ /* Get All Coupons Asynchronously: As the requests keep fulfilling response will be sent as callback */
+ async getAllCouponsAsync(
+ callback: (
+ data: CouponClass[],
+ isDone: boolean,
+ errorMessage?: ApiError
+ ) => void
+ ) {
+ /* API Request */
+ const apiRequest = new ApiRequest(this.BASE_URL);
+
+ /* Initializing Page Number Counter */
+ let pageNumberCounter = this.defaultPageNumber;
+
+ /* First Request to know the total pages */
+ const firstResponse = await apiRequest.getRequest({
+ page: pageNumberCounter,
+ limit: this.defaultPageLimit,
+ });
+
+ /* If the first request is successful */
+ if (firstResponse instanceof ApiResponse && firstResponse.success) {
+ /* Total Number of Pages */
+ const totalPages = firstResponse.data.totalPages;
+
+ /* Incrementing page counter */
+ pageNumberCounter++;
+
+ /* Number of requests to be made */
+ let requestsPending = totalPages - pageNumberCounter + 1;
+
+ /* If first request is the last request: return */
+ if (!requestsPending) {
+ return callback(firstResponse.data.coupons, true);
+ } else {
+ callback(firstResponse.data.coupons, false);
+ }
+
+ /* Remaining requests made in parallel */
+ for (let counter = pageNumberCounter; counter <= totalPages; counter++) {
+ apiRequest
+ .getRequest({
+ page: counter,
+ limit: this.defaultPageLimit,
+ })
+ .then((res) => {
+ /* Decrementing pending requests count */
+ requestsPending--;
+
+ /* Error in request: Return */
+ if (!(res instanceof ApiResponse && res.success)) {
+ return res instanceof ApiError
+ ? callback([], true, res)
+ : callback([], true, new ApiError(res.message));
+ } else if (!requestsPending) {
+ /* All Requests are done */
+ return callback(res.data.coupons, true);
+ } else {
+ /* Sending the data of an in between request */
+ callback(res.data.coupons, false);
+ }
+ });
+ }
+ } else {
+ // Error
+ return firstResponse instanceof ApiError
+ ? callback([], true, firstResponse)
+ : callback([], true, new ApiError(firstResponse.message));
+ }
+ }
+
/* Get All Coupons Available to User Asynchronously: As the requests keep fulfilling response will be sent as callback */
async getAllCouponsAvailableToUserAsync(
callback: (
@@ -105,6 +178,84 @@ class CouponService {
return response;
}
}
+
+ async updateCouponActiveStatus(
+ couponId: string,
+ isActive: boolean
+ ): Promise {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/status/${couponId}`);
+
+ const response = await apiRequest.patchRequest({ isActive });
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async addCoupon(
+ fields: AddEditCouponFields
+ ): Promise {
+ const apiRequest = new ApiRequest(this.BASE_URL);
+
+ const response = await apiRequest.postRequest({
+ name: fields.name,
+ couponCode: fields.couponCode,
+ type: COUPON_TYPES.FLAT,
+ discountValue: fields.discountValue,
+ minimumCartValue: fields.minimumCartValue,
+ expiryDate: getMomentObjectFromDateObject(fields.expiryDate).format(
+ DATE_TIME_FORMATS.standardDateWithTime
+ ),
+ startDate: getMomentObjectFromDateObject(fields.startDate).format(
+ DATE_TIME_FORMATS.standardDateWithTime
+ ),
+ });
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async editCoupon(
+ fields: AddEditCouponFields,
+ couponId: string
+ ): Promise {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/${couponId}`);
+
+ const response = await apiRequest.patchRequest({
+ name: fields.name,
+ couponCode: fields.couponCode,
+ discountValue: fields.discountValue,
+ });
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async deleteCoupon(couponId: string): Promise {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/${couponId}`);
+
+ const response = await apiRequest.deleteRequest<{
+ deletedCoupon: CouponClass;
+ }>();
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data.deletedCoupon;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
}
export default new CouponService();
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/order/OrderService.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/order/OrderService.ts
index 746e2df6..b92bcd76 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/order/OrderService.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/order/OrderService.ts
@@ -4,12 +4,17 @@ import ApiRequest from "../ApiRequest";
import ApiResponse from "../ApiResponse";
import {
GeneratePayPalOrderResponseClass,
- OrderDetailClass
+ OrderClass,
+ OrderDetailClass,
+ OrderListClass,
} from "./OrderTypes";
+import { ORDER_STATUS } from "../../data/applicationData";
class OrderService {
BASE_ORDERS_URL = "/api/v1/ecommerce/orders";
BASE_URL = "/api/v1/ecommerce/orders/provider";
+ defaultPageNumber = 1;
+ defaultPageLimit = 50;
async generatePayPalOrder(
addressId: string
@@ -54,6 +59,83 @@ class OrderService {
}
return response;
}
+
+ async getOrdersAsync(
+ status: ORDER_STATUS | undefined,
+ callback: (data: OrderClass[], isDone: boolean, error?: ApiError) => void
+ ) {
+ const apiRequest = new ApiRequest(`${this.BASE_ORDERS_URL}/list/admin`);
+
+ let page = this.defaultPageNumber;
+
+ const firstResponse = await apiRequest.getRequest({
+ page,
+ limit: this.defaultPageLimit,
+ status: status,
+ });
+
+ if (firstResponse instanceof ApiResponse && firstResponse.success) {
+ const totalPages = firstResponse.data.totalPages;
+
+ page++;
+ let requestsPending = totalPages - page + 1;
+
+ if (!requestsPending) {
+ return callback(firstResponse.data.orders, true);
+ } else {
+ callback(firstResponse.data.orders, false);
+ }
+
+ for (page; page <= totalPages; page++) {
+ apiRequest
+ .getRequest({
+ page,
+ limit: this.defaultPageLimit,
+ status: status,
+ })
+ .then((res) => {
+ requestsPending--;
+
+ /* Error in request: Return */
+ if (!(res instanceof ApiResponse && res.success)) {
+ return res instanceof ApiError
+ ? callback([], true, res)
+ : callback([], true, new ApiError(res.message));
+ } else if (!requestsPending) {
+ /* All Requests are done */
+ return callback(res.data.orders, true);
+ } else {
+ /* Sending the data of an in between request */
+ callback(res.data.orders, false);
+ }
+ });
+ }
+ } else if (firstResponse instanceof ApiResponse) {
+ return callback([], false, new ApiError(firstResponse.message));
+ } else {
+ return callback([], false, firstResponse);
+ }
+ }
+
+ async editOrderStatus(
+ status: ORDER_STATUS,
+ orderId: string
+ ): Promise<{ status: ORDER_STATUS } | ApiError> {
+ const apiRequest = new ApiRequest(
+ `${this.BASE_ORDERS_URL}/status/${orderId}`
+ );
+
+ const response = await apiRequest.patchRequest<{ status: ORDER_STATUS }>({
+ status,
+ });
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
}
export default new OrderService();
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/product/ProductService.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/product/ProductService.ts
index 254657f6..8bbc6a97 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/product/ProductService.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/services/product/ProductService.ts
@@ -1,3 +1,7 @@
+import {
+ AddEditProductFieldsForService,
+ EditProductFieldsForService,
+} from "../../constants";
import { generateRandomNumber } from "../../utils/commonHelper";
import ApiError from "../ApiError";
import ApiRequest from "../ApiRequest";
@@ -115,7 +119,12 @@ class ProductService {
}
async getAllProductsAsync(
- callback: (data: Array, isDone: boolean, error?: ApiError, categoryInfo?: {_id: string, name: string}) => void,
+ callback: (
+ data: Array,
+ isDone: boolean,
+ error?: ApiError,
+ categoryInfo?: { _id: string; name: string }
+ ) => void,
categoryId?: string
) {
/*
@@ -146,9 +155,19 @@ class ProductService {
/* If no requests are pending return else send intermediate response */
if (!requestsPending) {
- return callback(firstResponse.data.products, true, undefined, firstResponse.data.category);
+ return callback(
+ firstResponse.data.products,
+ true,
+ undefined,
+ firstResponse.data.category
+ );
} else {
- callback(firstResponse.data.products, false, undefined, firstResponse.data.category);
+ callback(
+ firstResponse.data.products,
+ false,
+ undefined,
+ firstResponse.data.category
+ );
}
for (let counter = page; counter <= totalPages; counter++) {
apiRequest
@@ -164,10 +183,20 @@ class ProductService {
: callback([], true, new ApiError(response.message));
} else if (!requestsPending) {
/* All Requests are done */
- return callback(response.data.products, true, undefined, response.data.category);
+ return callback(
+ response.data.products,
+ true,
+ undefined,
+ response.data.category
+ );
} else {
/* Sending the data of an in between request */
- callback(response.data.products, false, undefined, response.data.category);
+ callback(
+ response.data.products,
+ false,
+ undefined,
+ response.data.category
+ );
}
});
}
@@ -218,6 +247,127 @@ class ProductService {
return response;
}
}
+
+ async addProduct(
+ fields: AddEditProductFieldsForService
+ ): Promise {
+ const apiRequest = new ApiRequest(this.BASE_URL);
+
+ /* Form data as body */
+ const productFormData = new FormData();
+ productFormData.append("name", fields.name);
+ productFormData.append("description", fields.description);
+ productFormData.append("price", fields.price.toString());
+ productFormData.append("stock", fields.stock.toString());
+ productFormData.append("category", fields.category.id as string);
+ productFormData.append("mainImage", fields.mainImage);
+
+ fields.subImages.forEach((subImage) => {
+ productFormData.append("subImages", subImage);
+ });
+
+ /* API Call */
+ const response = await apiRequest.postRequest(productFormData);
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ /* Edit Product */
+ async editProduct(
+ fields: EditProductFieldsForService,
+ productId: string
+ ): Promise {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/${productId}`);
+
+ /* Form data */
+ const formData = new FormData();
+
+ let key: keyof typeof fields;
+ for (key in fields) {
+ const value = fields[key];
+ if (value !== undefined) {
+ /* For string types: name, description */
+
+ if (typeof value === "string") {
+ formData.append(key, value);
+ }
+ else if (typeof value === "number") {
+ /* Number types: price, stock */
+
+ formData.append(key, value.toString());
+ }
+ else if (
+
+ /* Category */
+ "id" in value &&
+ "text" in value &&
+ typeof value.id === "string"
+ )
+ {
+ /* Dropdown Item */
+ formData.append(key, value.id);
+ }
+ else if (value instanceof File) {
+ /* mainImage */
+ formData.append(key, value);
+ }
+ else if (Array.isArray(value)) {
+ /* Sub Images */
+ value.forEach((subImage) => {
+ formData.append(key, subImage);
+ });
+ }
+ }
+ }
+
+ const response = await apiRequest.patchRequest(formData);
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+
+ return response;
+ }
+
+ /* Remove a sub image of the product */
+ async removeSubImageOfProduct(
+ subImageId: string,
+ productId: string
+ ): Promise {
+ const apiRequest = new ApiRequest(
+ `${this.BASE_URL}/remove/subimage/${productId}/${subImageId}`
+ );
+
+ const response = await apiRequest.patchRequest({});
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
+
+ async deleteProduct(productId: string): Promise {
+ const apiRequest = new ApiRequest(`${this.BASE_URL}/${productId}`);
+
+ const response = await apiRequest.deleteRequest<{
+ deletedProduct: Product;
+ }>();
+
+ if (response instanceof ApiResponse && response.success) {
+ return response.data.deletedProduct;
+ } else if (response instanceof ApiResponse) {
+ return new ApiError(response.message);
+ }
+ return response;
+ }
}
export default new ProductService();
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/UIinfoSlice.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/UIinfoSlice.ts
new file mode 100644
index 00000000..42b65064
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/UIinfoSlice.ts
@@ -0,0 +1,17 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+const initialState = {
+ headerHeight: 0,
+};
+const UIinfoSlice = createSlice({
+ name: "UIinfoSlic",
+ initialState,
+ reducers: {
+ updateHeaderHeight(state, { payload }) {
+ state.headerHeight = payload;
+ },
+ },
+});
+
+export const { updateHeaderHeight } = UIinfoSlice.actions;
+export default UIinfoSlice;
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/index.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/index.ts
index 54e3f48e..61a23966 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/index.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/store/index.ts
@@ -5,6 +5,7 @@ import BreakpointSlice from "./BreakpointSlice";
import AuthSlice from "./AuthSlice";
import ToastMessageSlice from "./ToastMessageSlice";
import CartSlice from "./CartSlice";
+import UIinfoSlice from "./UIinfoSlice";
const store = configureStore({
reducer: {
@@ -12,7 +13,8 @@ const store = configureStore({
breakpoint: BreakpointSlice.reducer,
auth: AuthSlice.reducer,
toastMessage: ToastMessageSlice.reducer,
- cart: CartSlice.reducer
+ cart: CartSlice.reducer,
+ uiInfo: UIinfoSlice.reducer
}
})
/* useAppSelector and useAppDispatch for typescript */
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/styles/Grid.css b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/styles/Grid.css
new file mode 100644
index 00000000..b30bd7e7
--- /dev/null
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/styles/Grid.css
@@ -0,0 +1,4 @@
+/* Hiding dropdown in the column filter */
+.ag-filter .ag-picker-field {
+ display: none;
+}
\ No newline at end of file
diff --git a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/utils/dateTimeHelper.ts b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/utils/dateTimeHelper.ts
index 4131ea9c..3bad8c98 100644
--- a/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/utils/dateTimeHelper.ts
+++ b/examples/apps/ecommerce/web/react-vite-redux-tailwind/src/utils/dateTimeHelper.ts
@@ -73,3 +73,38 @@ export const checkIfDateIsInRange = (
}
return false;
};
+
+
+/* Grid Date Comparator for filter */
+export const gridDateFilterComparator = (filterLocalDateAtMidnight: Date, cellValue: DATE_TIME_FORMATS.displayedDateWithTime) => {
+
+ const filterValueMoment = moment(filterLocalDateAtMidnight);
+ const cellValueMoment = moment(cellValue, DATE_TIME_FORMATS.displayedDateWithTime);
+ if(filterValueMoment.isSame(cellValueMoment, 'date')){
+ return 0;
+ }
+ else if(filterValueMoment.isAfter(cellValueMoment, 'date')){
+ return -1;
+ }
+
+ return 1;
+
+}
+/* Grid Date Comparator for sorting */
+export const gridDateSortComparator = (dateA: DATE_TIME_FORMATS.displayedDateWithTime, dateB: DATE_TIME_FORMATS.displayedDateWithTime) => {
+
+ const dateAMoment = moment(dateA, DATE_TIME_FORMATS.displayedDateWithTime);
+ const dateBMoment = moment(dateB, DATE_TIME_FORMATS.displayedDateWithTime);
+
+ if(dateAMoment.isSame(dateBMoment)){
+ return 0;
+ }
+ else if(dateAMoment.isAfter(dateBMoment)){
+ return -1;
+ }
+ return 1;
+}
+
+export const getMomentObjectFromDateObject = (date: Date) => {
+ return moment(date);
+}
\ No newline at end of file