From b329c8422de2872a661d0aca06cf5bf96adc09f0 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Sun, 20 Jul 2025 23:34:42 +0100 Subject: [PATCH] oh --- app/dashboard/orders/[id]/page.tsx | 55 ++++++- app/dashboard/products/page.tsx | 1 - components/dashboard/BuyerOrderInfo.tsx | 72 ++++++---- .../notifications/UnifiedNotifications.tsx | 17 ++- components/tables/order-table.tsx | 57 +++++++- lib/api-client.ts | 134 +++++++++++++++++- 6 files changed, 294 insertions(+), 42 deletions(-) diff --git a/app/dashboard/orders/[id]/page.tsx b/app/dashboard/orders/[id]/page.tsx index d76c475..dcb2d70 100644 --- a/app/dashboard/orders/[id]/page.tsx +++ b/app/dashboard/orders/[id]/page.tsx @@ -22,7 +22,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Clipboard, Truck, Package, ArrowRight, ChevronDown, AlertTriangle, Copy } from "lucide-react"; +import { Clipboard, Truck, Package, ArrowRight, ChevronDown, AlertTriangle, Copy, Loader2, RefreshCw } from "lucide-react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { @@ -37,6 +37,7 @@ import { AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import Layout from "@/components/layout/layout"; +import { cacheUtils } from '@/lib/api-client'; interface Order { orderId: string; @@ -128,6 +129,7 @@ export default function OrderDetailsPage() { const [currentPage, setCurrentPage] = useState(1); const [isAcknowledging, setIsAcknowledging] = useState(false); const [isCancelling, setIsCancelling] = useState(false); + const [refreshTrigger, setRefreshTrigger] = useState(0); const router = useRouter(); const params = useParams(); @@ -182,8 +184,23 @@ export default function OrderDetailsPage() { if (response && response.message === "Order status updated successfully") { // Update both states setIsPaid(true); - setOrder((prevOrder) => (prevOrder ? { ...prevOrder, status: "paid" } : null)); + setOrder((prevOrder) => (prevOrder ? { + ...prevOrder, + status: "paid", + // Clear underpayment flags when marking as paid + underpaid: false, + underpaymentAmount: 0 + } : null)); + + // Invalidate order cache to ensure fresh data everywhere + cacheUtils.invalidateOrderData(orderId as string); + toast.success("Order marked as paid successfully"); + + // Refresh order data to get latest status + setTimeout(() => { + setRefreshTrigger(prev => prev + 1); + }, 500); } else { // Handle unexpected response format console.error("Unexpected response format:", response); @@ -336,7 +353,7 @@ export default function OrderDetailsPage() { const data: Order = await res; setOrder(data); - console.log(data); + console.log("Fresh order data:", data); const productIds = data.products.map((product) => product.productId); const productNamesMap = await fetchProductNames(productIds, authToken); @@ -368,7 +385,7 @@ export default function OrderDetailsPage() { }; fetchOrderDetails(); - }, [orderId]); + }, [orderId, refreshTrigger]); useEffect(() => { const fetchAdjacentOrders = async () => { @@ -480,7 +497,13 @@ export default function OrderDetailsPage() { // Helper function to check if order is underpaid const isOrderUnderpaid = (order: Order | null) => { - return order?.underpaid === true && order?.underpaymentAmount && order.underpaymentAmount > 0; + // More robust check - only show underpaid if status is NOT paid and underpayment exists + return order?.underpaid === true && + order?.underpaymentAmount && + order.underpaymentAmount > 0 && + order.status !== "paid" && + order.status !== "completed" && + order.status !== "shipped"; }; // Helper function to get underpaid information @@ -501,6 +524,15 @@ export default function OrderDetailsPage() { const underpaidInfo = getUnderpaidInfo(order); + // Add order refresh subscription + useEffect(() => { + const unsubscribe = cacheUtils.onOrderRefresh(() => { + setRefreshTrigger(prev => prev + 1); + }); + + return unsubscribe; + }, []); + if (loading) return ( @@ -537,6 +569,19 @@ export default function OrderDetailsPage() {
+ {prevOrderId && ( - + {loading ? ( -
- +
+ Loading orders...
) : orders.length === 0 ? ( -
- -

No orders found

+
+ No orders found
) : ( <> -
-
-

Recent Orders

-
- - {orders.length} {orders.length === 1 ? 'order' : 'orders'} -
-
+
+

Customer Orders

+

+ {orders.length} {orders.length === 1 ? 'order' : 'orders'} • Total: {formatPrice( + orders.reduce((sum, order) => sum + order.totalPrice, 0) + )} +

- {/* Show orders */}
{orders.map((order) => (
{ + // Only include orders that are actually fully paid (not underpaid) + return order.status === 'paid' && + (!order.underpaid || order.underpaymentAmount === 0); + }); + // If this is the first fetch, just store the orders without notifications if (isInitialOrdersFetch.current) { - orders.forEach(order => seenOrderIds.current.add(order._id)); + validPaidOrders.forEach(order => seenOrderIds.current.add(order._id)); isInitialOrdersFetch.current = false; return; } // Check for new paid orders that haven't been seen before - const latestNewOrders = orders.filter(order => !seenOrderIds.current.has(order._id)); + const latestNewOrders = validPaidOrders.filter(order => !seenOrderIds.current.has(order._id)); // Show notifications for new orders if (latestNewOrders.length > 0) { @@ -178,6 +188,9 @@ export default function UnifiedNotifications() { // Update the state with new orders for the dropdown setNewOrders(prev => [...latestNewOrders, ...prev].slice(0, 10)); + + // Invalidate order cache to ensure all components refresh + cacheUtils.invalidateOrderData(); } } catch (error) { console.error("Error checking for new orders:", error); diff --git a/components/tables/order-table.tsx b/components/tables/order-table.tsx index 5063b79..83f78af 100644 --- a/components/tables/order-table.tsx +++ b/components/tables/order-table.tsx @@ -45,6 +45,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; +import { cacheUtils } from '@/lib/api-client'; interface Order { _id: string; @@ -126,6 +127,17 @@ export default function OrderTable() { const [isShipping, setIsShipping] = useState(false); const [itemsPerPage, setItemsPerPage] = useState(20); const pageSizeOptions = [5, 10, 15, 20, 25, 50, 75, 100]; + const [refreshTrigger, setRefreshTrigger] = useState(0); + + // Add order refresh subscription + useEffect(() => { + const unsubscribe = cacheUtils.onOrderRefresh(() => { + console.log("Order data refresh triggered in OrderTable"); + setRefreshTrigger(prev => prev + 1); + }); + + return unsubscribe; + }, []); // Fetch orders with server-side pagination const fetchOrders = useCallback(async () => { @@ -141,6 +153,7 @@ export default function OrderTable() { const data = await clientFetch(`/orders?${queryParams}`); + console.log("Fetched orders with fresh data:", data.orders?.length || 0); setOrders(data.orders || []); setTotalPages(data.totalPages || 1); setTotalOrders(data.totalOrders || 0); @@ -150,7 +163,7 @@ export default function OrderTable() { } finally { setLoading(false); } - }, [currentPage, statusFilter, itemsPerPage, sortConfig]); + }, [currentPage, statusFilter, itemsPerPage, sortConfig, refreshTrigger]); useEffect(() => { fetchOrders(); @@ -288,7 +301,14 @@ export default function OrderTable() { // Helper function to determine if order is underpaid const isOrderUnderpaid = (order: Order) => { - return order.underpaid === true && order.underpaymentAmount && order.underpaymentAmount > 0; + // More robust check - only show underpaid if status allows it and underpayment exists + return order.underpaid === true && + order.underpaymentAmount && + order.underpaymentAmount > 0 && + order.status !== "paid" && + order.status !== "completed" && + order.status !== "shipped" && + order.status !== "cancelled"; }; // Helper function to get underpaid display info @@ -307,6 +327,29 @@ export default function OrderTable() { }; }; + // Add manual refresh function + const handleRefresh = () => { + console.log("Manual refresh triggered"); + setRefreshTrigger(prev => prev + 1); + toast.success("Orders refreshed"); + }; + + // Add periodic refresh for underpaid orders + useEffect(() => { + // Check if we have any underpaid orders + const hasUnderpaidOrders = orders.some(order => isOrderUnderpaid(order)); + + if (hasUnderpaidOrders) { + console.log("Found underpaid orders, setting up refresh interval"); + const interval = setInterval(() => { + console.log("Auto-refreshing due to underpaid orders"); + setRefreshTrigger(prev => prev + 1); + }, 30000); // Refresh every 30 seconds if there are underpaid orders + + return () => clearInterval(interval); + } + }, [orders]); + return (
@@ -409,11 +452,11 @@ export default function OrderTable() {
£{order.totalPrice.toFixed(2)} - {underpaidInfo && ( - - Missing: £{(underpaidInfo.missing * 100).toFixed(2)} - - )} + {underpaidInfo && ( + + Missing: £{(underpaidInfo.missing * 100).toFixed(2)} + + )}
diff --git a/lib/api-client.ts b/lib/api-client.ts index 4de125e..7f37802 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -47,6 +47,96 @@ export interface CustomerResponse { success?: boolean; } +// Add cache invalidation and order refresh utilities +interface CacheEntry { + data: any; + timestamp: number; + ttl: number; // Time to live in milliseconds +} + +class ApiCache { + private cache = new Map(); + private orderRefreshCallbacks = new Set<() => void>(); + + get(key: string): any | null { + const entry = this.cache.get(key); + if (!entry) return null; + + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + return null; + } + + return entry.data; + } + + set(key: string, data: any, ttl: number = 60000): void { // Default 1 minute TTL + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl + }); + } + + invalidate(pattern?: string): void { + if (pattern) { + // Remove entries matching pattern + for (const [key] of this.cache) { + if (key.includes(pattern)) { + this.cache.delete(key); + } + } + } else { + // Clear all cache + this.cache.clear(); + } + } + + // Order-specific cache invalidation + invalidateOrderData(orderId?: string): void { + if (orderId) { + this.invalidate(`orders/${orderId}`); + this.invalidate(`orders?`); // Invalidate order lists + } else { + this.invalidate('orders'); + } + + // Show a subtle notification when data is refreshed + if (typeof window !== 'undefined') { + console.log('🔄 Order data cache invalidated, refreshing components...'); + } + + // Trigger order refresh callbacks + this.orderRefreshCallbacks.forEach(callback => { + try { + callback(); + } catch (error) { + console.error('Error in order refresh callback:', error); + } + }); + } + + // Register callback for order data refresh + onOrderRefresh(callback: () => void): () => void { + this.orderRefreshCallbacks.add(callback); + + // Return unsubscribe function + return () => { + this.orderRefreshCallbacks.delete(callback); + }; + } +} + +// Global cache instance +const apiCache = new ApiCache(); + +// Export cache utilities +export const cacheUtils = { + invalidateOrderData: (orderId?: string) => apiCache.invalidateOrderData(orderId), + onOrderRefresh: (callback: () => void) => apiCache.onOrderRefresh(callback), + invalidateAll: () => apiCache.invalidate(), +}; + /** * Normalizes a URL to ensure it has the correct /api prefix * This prevents double prefixing which causes API errors @@ -261,4 +351,46 @@ export const getCustomers = async (page: number = 1, limit: number = 25): Promis */ export const getCustomerDetails = async (userId: string): Promise => { return clientFetch(`/customers/${userId}`); -}; \ No newline at end of file +}; + +/** + * Enhanced client-side fetch function with caching and automatic invalidation + */ +export async function clientFetchWithCache( + url: string, + options: RequestInit = {}, + cacheKey?: string, + ttl?: number +): Promise { + // Check cache first for GET requests + if (options.method === 'GET' || !options.method) { + const cached = apiCache.get(cacheKey || url); + if (cached) { + return cached; + } + } + + // Make the request + const result = await clientFetch(url, options); + + // Cache GET requests + if ((options.method === 'GET' || !options.method) && cacheKey) { + apiCache.set(cacheKey, result, ttl); + } + + // Invalidate cache for mutations that affect orders + if (options.method && ['PUT', 'POST', 'DELETE', 'PATCH'].includes(options.method)) { + if (url.includes('/orders/') && url.includes('/status')) { + // Order status update - invalidate order cache + const orderIdMatch = url.match(/\/orders\/([^\/]+)\/status/); + if (orderIdMatch) { + apiCache.invalidateOrderData(orderIdMatch[1]); + } + } else if (url.includes('/orders')) { + // General order mutation + apiCache.invalidateOrderData(); + } + } + + return result; +} \ No newline at end of file