Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions context/AppContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -36,6 +37,8 @@ interface IAppContextProps {
setToggleNotification: React.Dispatch<SetStateAction<boolean>>;
setNotificationsCount: React.Dispatch<SetStateAction<number>>;
notificationsCount: number;
ordersCount: number;
setOrdersCount: React.Dispatch<SetStateAction<number>>;
};

const initialState: IAppContextProps = {
Expand All @@ -57,7 +60,9 @@ const initialState: IAppContextProps = {
toggleNotification: false,
setToggleNotification: () => {},
setNotificationsCount: () => {},
notificationsCount: 0
notificationsCount: 0,
ordersCount: 0,
setOrdersCount: () => {},
};

export const AppContext = createContext<IAppContextProps>(initialState);
Expand All @@ -77,6 +82,7 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => {
const [adsSupported, setAdsSupported] = useState(false);
const [toggleNotification, setToggleNotification] = useState<boolean>(true);
const [notificationsCount, setNotificationsCount] = useState(0);
const [ordersCount, setOrdersCount] = useState(0);

useEffect(() => {
logger.info('AppContextProvider mounted.');
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -220,7 +246,9 @@ const AppContextProvider = ({ children }: AppContextProviderProps) => {
toggleNotification,
setToggleNotification,
setNotificationsCount,
notificationsCount
notificationsCount,
ordersCount,
setOrdersCount,
}}
>
{children}
Expand Down
24 changes: 21 additions & 3 deletions src/app/[locale]/seller/order-fulfillment/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
142 changes: 109 additions & 33 deletions src/app/[locale]/user/order-review/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<boolean>(true);
const [orderList, setOrderList] = useState<PartialOrderType[] >([]);

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<boolean>(false);
const [orderList, setOrderList] = useState<PartialOrderType[]>([]);
const [skip, setSkip] = useState(0);
const [limit] = useState(5);
const [hasFetched, setHasFetched] = useState(false);
const [hasMore, setHasMore] = useState(true);

const loadMoreRef = useRef<HTMLDivElement | null>(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const observer = useRef<IntersectionObserver | null>(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<string>();
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 (
<Skeleton type="seller_review" />
);
}
useScrollablePagination({
containerRef: scrollContainerRef,
loadMoreRef,
fetchNextPage: async () => {
setLoading(true);
await fetchOrders();
},
hasMore,
isLoading: loading,
});

return (
<>
Expand All @@ -64,10 +125,20 @@ export default function OrderReviewPage() {
</div>

{/* Review Order | Online Shopping */}
<div>
{orderList && orderList.length>0 && orderList.map((item, index)=>(
<Link href={`/${locale}/user/order-item/${item._id}?user_name=${currentUser?.user_name}`} key={index} >
<div
ref={scrollContainerRef}
id="order-scroll-container"
className="max-h-[600px] overflow-y-auto p-1 mb-7 mt-3"
>
{!loading && hasFetched && orderList.length === 0 ? (
<h2 className="font-bold mb-2 text-center">
No orders found
</h2>
) : (
orderList.map((item, index)=>(
<Link href={`/${locale}/user/order-item/${item._id}?user_name=${currentUser?.user_name}`} key={item._id} >
<div
ref={handleOrderItemRef}
data-id={item._id}
className={`relative outline outline-50 outline-gray-600 rounded-lg mb-7
${item.status === OrderStatusType.Completed ? 'bg-yellow-100' : item.status === OrderStatusType.Cancelled ?
Expand Down Expand Up @@ -133,7 +204,12 @@ export default function OrderReviewPage() {
</div>
</div>
</Link>
))}
))
)}

{/* Load more trigger */}
{loading && <Skeleton type="seller_review" />}
<div ref={loadMoreRef} className="h-[20px]" />
</div>
</div>
</>
Expand Down
17 changes: 10 additions & 7 deletions src/components/shared/Seller/OrderList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@ export const ListOrder: React.FC<{
const locale = useLocale();
const t = useTranslations();

const [orderList, setOrderList] = useState<PartialOrderType[] >([]);
const [orderList, setOrderList] = useState<PartialOrderType[]>([]);
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 (
Expand Down
5 changes: 3 additions & 2 deletions src/components/shared/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ function Navbar() {
alertMessage,
isSaveLoading,
userMembership,
notificationsCount
notificationsCount,
ordersCount
} = useContext(AppContext);

// check if the current page is the homepage
Expand Down Expand Up @@ -121,7 +122,7 @@ function Navbar() {
size={24}
className={`${isSigningInUser || isSaveLoading ? 'text-tertiary cursor-not-allowed' : 'text-secondary'}`}
/>
{notificationsCount > 0 && (
{(notificationsCount > 0 || ordersCount > 0) && (
<span className="absolute top-[-6px] right-[-6px] w-[10px] h-[10px] bg-red-500 rounded-full animate-pulse" />
)}
</>
Expand Down
Loading