diff --git a/context/AppContextProvider.tsx b/context/AppContextProvider.tsx index 10ca9af4..40aa8c46 100644 --- a/context/AppContextProvider.tsx +++ b/context/AppContextProvider.tsx @@ -14,6 +14,7 @@ import { onIncompletePaymentFound } from '@/config/payment'; import { AuthResult } from '@/constants/pi'; import { IUser, MembershipClassType } from '@/constants/types'; import { getNotifications } from '@/services/notificationApi'; +import { getOrders } from '@/services/orderApi'; import logger from '../logger.config.mjs'; interface IAppContextProps { @@ -36,6 +37,8 @@ interface IAppContextProps { setToggleNotification: React.Dispatch>; setNotificationsCount: React.Dispatch>; notificationsCount: number; + ordersCount: number; + setOrdersCount: React.Dispatch>; }; const initialState: IAppContextProps = { @@ -57,7 +60,9 @@ const initialState: IAppContextProps = { toggleNotification: false, setToggleNotification: () => {}, setNotificationsCount: () => {}, - notificationsCount: 0 + notificationsCount: 0, + ordersCount: 0, + setOrdersCount: () => {}, }; export const AppContext = createContext(initialState); @@ -77,6 +82,7 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => { const [adsSupported, setAdsSupported] = useState(false); const [toggleNotification, setToggleNotification] = useState(true); const [notificationsCount, setNotificationsCount] = useState(0); + const [ordersCount, setOrdersCount] = useState(0); useEffect(() => { logger.info('AppContextProvider mounted.'); @@ -115,6 +121,26 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => { fetchNotificationsCount(); }, [currentUser, reload]); + useEffect(() => { + if (!currentUser) return; + + const fetchOrdersCount = async () => { + try { + const { count } = await getOrders({ + skip: 0, + limit: 1, + status: 'pending' + }); + setOrdersCount(count); + } catch (error) { + logger.error('Failed to fetch orders count:', error); + setOrdersCount(0); + } + }; + + fetchOrdersCount(); + }, [currentUser, reload]); + const showAlert = (message: string) => { setAlertMessage(message); setTimeout(() => { @@ -220,7 +246,9 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => { toggleNotification, setToggleNotification, setNotificationsCount, - notificationsCount + notificationsCount, + ordersCount, + setOrdersCount, }} > {children} diff --git a/src/app/[locale]/seller/order-fulfillment/[id]/page.tsx b/src/app/[locale]/seller/order-fulfillment/[id]/page.tsx index 41bb5159..990f3a98 100644 --- a/src/app/[locale]/seller/order-fulfillment/[id]/page.tsx +++ b/src/app/[locale]/seller/order-fulfillment/[id]/page.tsx @@ -2,16 +2,17 @@ import { useTranslations, useLocale } from "next-intl"; import Image from "next/image"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { Button } from "@/components/shared/Forms/Buttons/Buttons"; import { Input, Select, TextArea } from "@/components/shared/Forms/Inputs/Inputs"; import { OrderItemStatus, OrderItemType, OrderStatusType, PartialOrderType } from "@/constants/types"; -import { fetchOrderById, updateOrderStatus, updateOrderItemStatus } from "@/services/orderApi"; +import { fetchOrderById, updateOrderStatus, updateOrderItemStatus, getOrders } from "@/services/orderApi"; import { resolveDate } from "@/utils/date"; import { getFulfillmentMethodOptions, translateSellerCategory } from "@/utils/translate"; +import { AppContext } from '../../../../../../context/AppContextProvider'; import logger from '../../../../../../logger.config.mjs'; export default function OrderItemPage({ params, searchParams }: { params: { id: string }, searchParams: { seller_name: string, seller_type: string } }) { @@ -20,6 +21,7 @@ export default function OrderItemPage({ params, searchParams }: { params: { id: const locale = useLocale(); const t = useTranslations(); + const { setOrdersCount } = useContext(AppContext); const orderId = params.id; const sellerName = searchParams.seller_name; @@ -72,6 +74,17 @@ export default function OrderItemPage({ params, searchParams }: { params: { id: }; const handleCompleted = async (status: OrderStatusType) => { + const prev = currentOrder; + if (!prev) return; + + // Optimistic local update + setIsCompleted(true); + + // If the previous status was Pending and now it's Completed, decrement the counter + if (prev.status === OrderStatusType.Pending && status === OrderStatusType.Completed) { + setOrdersCount((c) => Math.max(0, c - 1)); // optimistic badge update + } + try { logger.info(`Updating order status to ${status} with id: ${orderId}`); const data = await updateOrderStatus(orderId, status); @@ -80,9 +93,14 @@ export default function OrderItemPage({ params, searchParams }: { params: { id: setCurrentOrder(data.order); setOrderItems(data.orderItems); setBuyerName(data.pi_username); - setIsCompleted(true); + + // Background sync with BE (optional, to avoid drift) + const { count } = await getOrders({ skip: 0, limit: 1, status: 'pending' }); + setOrdersCount(count); } else { logger.warn("Failed to update completed order on the server."); + // Rollback if API fails + setIsCompleted(false); } } catch (error) { diff --git a/src/app/[locale]/user/order-review/page.tsx b/src/app/[locale]/user/order-review/page.tsx index 5e277efc..b2d504aa 100644 --- a/src/app/[locale]/user/order-review/page.tsx +++ b/src/app/[locale]/user/order-review/page.tsx @@ -2,11 +2,12 @@ import { useTranslations, useLocale } from 'next-intl'; import Link from 'next/link'; -import React, { useEffect, useState, useContext, } from 'react'; +import React, { useEffect, useState, useContext, useRef } from 'react'; import { Input } from '@/components/shared/Forms/Inputs/Inputs'; import Skeleton from '@/components/skeleton/skeleton'; import { PartialOrderType, OrderStatusType } from '@/constants/types'; import { fetchBuyerOrders } from '@/services/orderApi'; +import { useScrollablePagination } from '@/hooks/useScrollablePagination'; import { resolveDate } from '@/utils/date'; import { translateOrderStatusType } from '@/utils/translate'; @@ -19,37 +20,97 @@ export default function OrderReviewPage() { const HEADER = 'font-bold text-lg md:text-2xl'; - const { currentUser } = useContext(AppContext); - const [loading, setLoading] = useState(true); - const [orderList, setOrderList] = useState([]); - - useEffect(() => { - const getOrderList= async (id: string) => { - setLoading(true); - try { - const data = await fetchBuyerOrders(id); - if (data) { - setOrderList(data); - } else { - setOrderList([]); - } - } catch (error) { - logger.error('Error fetching buyer data:', error); - } finally { - setLoading(false); + const { currentUser, setOrdersCount } = useContext(AppContext); + const [loading, setLoading] = useState(false); + const [orderList, setOrderList] = useState([]); + const [skip, setSkip] = useState(0); + const [limit] = useState(5); + const [hasFetched, setHasFetched] = useState(false); + const [hasMore, setHasMore] = useState(true); + + const loadMoreRef = useRef(null); + const scrollContainerRef = useRef(null); + const observer = useRef(null); + + const handleOrderItemRef = (node: HTMLElement | null) => { + if (node && observer.current) { + observer.current.observe(node); + } + }; + + const sortOrders = ( + current: PartialOrderType[], + incoming: PartialOrderType[] + ): PartialOrderType[] => { + const seen = new Set(); + const pending: PartialOrderType[] = []; + const others: PartialOrderType[] = []; + + for (const order of [...current, ...incoming]) { + if (seen.has(order._id)) continue; + seen.add(order._id); + + if (order.status === OrderStatusType.Pending) { + pending.push(order); + } else { + others.push(order); } - }; - - getOrderList(currentUser?.pi_uid as string); + } + + return [...pending, ...others]; + }; + + const fetchOrders = async () => { + if (loading || !currentUser?.pi_uid || !hasMore) return; + + setLoading(true); + + try { + const { items, count } = await fetchBuyerOrders({ skip, limit }); + + logger.info('Fetched buyer orders:', { itemsLength: items.length, count }); + + if (items.length > 0) { + // Merge and sort orders; pending first, then others + setOrderList((prev) => sortOrders(prev, items as PartialOrderType[])); + + // Increment skip by number of new items + const newSkip = skip + items.length; + setSkip(newSkip); + + // Determine if there are more orders + setHasMore(newSkip < count); + } else { + // No items returned → no more orders + setHasMore(false); + } + } catch (error) { + logger.error('Error fetching buyer orders:', error); + } finally { + setHasFetched(true); + setLoading(false); + } + }; + + useEffect(() => { + if (!currentUser?.pi_uid) return; + + setOrderList([]); + setSkip(0); + setHasMore(true); + fetchOrders(); }, [currentUser?.pi_uid]); - // loading condition - if (loading) { - logger.info('Loading seller data..'); - return ( - - ); - } + useScrollablePagination({ + containerRef: scrollContainerRef, + loadMoreRef, + fetchNextPage: async () => { + setLoading(true); + await fetchOrders(); + }, + hasMore, + isLoading: loading, + }); return ( <> @@ -64,10 +125,20 @@ export default function OrderReviewPage() { {/* Review Order | Online Shopping */} -
- {orderList && orderList.length>0 && orderList.map((item, index)=>( - +
+ {!loading && hasFetched && orderList.length === 0 ? ( +

+ No orders found +

+ ) : ( + orderList.map((item, index)=>( +
- ))} + )) + )} + + {/* Load more trigger */} + {loading && } +
diff --git a/src/components/shared/Seller/OrderList.tsx b/src/components/shared/Seller/OrderList.tsx index ac495ad8..b01b1fc0 100644 --- a/src/components/shared/Seller/OrderList.tsx +++ b/src/components/shared/Seller/OrderList.tsx @@ -16,23 +16,26 @@ export const ListOrder: React.FC<{ const locale = useLocale(); const t = useTranslations(); - const [orderList, setOrderList] = useState([]); + const [orderList, setOrderList] = useState([]); + const [totalCount, setTotalCount] = useState(0); useEffect(() => { - const getOrderList= async (id: string) => { + const getOrderList= async () => { try { - const data = await fetchSellerOrders(id); - if (data) { - setOrderList(data); + const { items, count } = await fetchSellerOrders({ skip: 0, limit: 50 }); + if (items) { + setOrderList(items as PartialOrderType[]); + setTotalCount(count); } else { setOrderList([]); + setTotalCount(0); } } catch (error) { - logger.error('Error fetching order items data:', error); + logger.error('Error fetching seller order items data:', error); } }; - getOrderList(user_id); + getOrderList(); }, [user_id]); return ( diff --git a/src/components/shared/navbar/Navbar.tsx b/src/components/shared/navbar/Navbar.tsx index fd509c94..daad1552 100644 --- a/src/components/shared/navbar/Navbar.tsx +++ b/src/components/shared/navbar/Navbar.tsx @@ -33,7 +33,8 @@ function Navbar() { alertMessage, isSaveLoading, userMembership, - notificationsCount + notificationsCount, + ordersCount } = useContext(AppContext); // check if the current page is the homepage @@ -121,7 +122,7 @@ function Navbar() { size={24} className={`${isSigningInUser || isSaveLoading ? 'text-tertiary cursor-not-allowed' : 'text-secondary'}`} /> - {notificationsCount > 0 && ( + {(notificationsCount > 0 || ordersCount > 0) && ( )} diff --git a/src/components/shared/sidebar/sidebar.tsx b/src/components/shared/sidebar/sidebar.tsx index 65f358c7..12d400cb 100644 --- a/src/components/shared/sidebar/sidebar.tsx +++ b/src/components/shared/sidebar/sidebar.tsx @@ -60,6 +60,7 @@ function Sidebar(props: any) { const pathname = usePathname(); const locale = useLocale(); const router = useRouter(); + const { ordersCount } = useContext(AppContext); const Filters = [ { target: 'include_active_sellers', title: t('SIDE_NAVIGATION.SEARCH_FILTERS.INCLUDE_ACTIVE_SELLERS') }, @@ -403,19 +404,32 @@ function Sidebar(props: any) { {isOnlineShoppingEnabled && (
-
)} diff --git a/src/hooks/useScrollablePagination.ts b/src/hooks/useScrollablePagination.ts index 884e49c2..2197eefd 100644 --- a/src/hooks/useScrollablePagination.ts +++ b/src/hooks/useScrollablePagination.ts @@ -1,8 +1,8 @@ import { useEffect, useRef } from 'react'; interface ScrollablePaginationOptions { - containerRef: React.RefObject; - loadMoreRef: React.RefObject; + containerRef: React.RefObject; + loadMoreRef: React.RefObject; fetchNextPage: () => Promise; hasMore: boolean; isLoading: boolean; diff --git a/src/services/orderApi.ts b/src/services/orderApi.ts index 692d066c..ea8882c6 100644 --- a/src/services/orderApi.ts +++ b/src/services/orderApi.ts @@ -1,5 +1,5 @@ import axiosClient from "@/config/client"; -import { PickedItems } from "@/constants/types"; +import { PickedItems, OrderType } from "@/constants/types"; import logger from '../../logger.config.mjs'; // Create and update an Order @@ -22,19 +22,39 @@ export const createAndUpdateOrder = async (orderData: any, orderItems: PickedIte } }; -// Fetch all orders associated with the seller -export const fetchSellerOrders = async (sellerId: string) => { +// Fetch all orders associated with the seller with pagination +export const fetchSellerOrders = async ({ + skip, + limit, + status +}: { + skip: number; + limit: number; + status?: 'pending' | 'completed' | 'cancelled'; +}): Promise<{ items: OrderType[]; count: number }> => { try { - logger.info(`Fetching seller order list associated with sellerID: ${sellerId}`); - const response = await axiosClient.get(`/orders/seller-orders`); + const queryParams = new URLSearchParams({ + skip: skip.toString(), + limit: limit.toString(), + }); + if (status) { + queryParams.append('status', status); + } + + logger.info(`Fetching seller order list with skip: ${skip}, limit: ${limit}`); + const response = await axiosClient.get(`/orders/seller-orders?${queryParams}`); if (response.status === 200) { logger.info(`Fetch seller orders successful with Status ${response.status}`, { data: response.data }); - return response.data; + const { items, count } = response.data; + return { + items: items as OrderType[], + count, + }; } else { logger.error(`Fetch seller orders failed with Status ${response.status}`); - return null; + return { items: [], count: 0 }; } } catch (error) { logger.error('Fetch seller orders encountered an error:', error); @@ -42,19 +62,39 @@ export const fetchSellerOrders = async (sellerId: string) => { } }; -// Fetch all orders associated with the current buyer -export const fetchBuyerOrders = async (buyerId: string) => { +// Fetch all orders associated with the current buyer with pagination +export const fetchBuyerOrders = async ({ + skip, + limit, + status +}: { + skip: number; + limit: number; + status?: 'pending' | 'completed' | 'cancelled'; +}): Promise<{ items: OrderType[]; count: number }> => { try { - logger.info(`Fetching buyer order list associated with userID: ${buyerId}`); - const response = await axiosClient.get(`/orders/review/buyer-orders`); + const queryParams = new URLSearchParams({ + skip: skip.toString(), + limit: limit.toString(), + }); + if (status) { + queryParams.append('status', status); + } + + logger.info(`Fetching buyer order list with skip: ${skip}, limit: ${limit}`); + const response = await axiosClient.get(`/orders/review/buyer-orders?${queryParams}`); if (response.status === 200) { logger.info(`Fetch buyer orders successful with Status ${response.status}`, { data: response.data }); - return response.data; + const { items, count } = response.data; + return { + items: items as OrderType[], + count, + }; } else { logger.error(`Fetch buyer orders failed with Status ${response.status}`); - return null; + return { items: [], count: 0 }; } } catch (error) { logger.error('Fetch buyer orders encountered an error:', error); @@ -121,4 +161,40 @@ export const updateOrderItemStatus = async (itemId: string, itemStatus: string) logger.error('Update order item status encountered an error:', error); throw new Error('Failed to update order item. Please try again later.'); } +}; + +// Add this function after the existing imports +export const getOrders = async ({ + skip, + limit, + status +}: { + skip: number; + limit: number; + status?: 'pending' | 'completed' | 'cancelled'; +}): Promise<{ items: OrderType[]; count: number }> => { + try { + const queryParams = new URLSearchParams({ + skip: skip.toString(), + limit: limit.toString(), + }); + if (status) { + queryParams.append('status', status); + } + + const response = await axiosClient.get(`/orders?${queryParams}`); + + if (response.status === 200) { + const { items, count } = response.data; + return { + items: items as OrderType[], + count, + }; + } else { + return { items: [], count: 0 }; + } + } catch (error) { + logger.error('Get orders encountered an error:', error); + throw new Error('Failed to get orders. Please try again later.'); + } }; \ No newline at end of file