This commit is contained in:
NotII
2025-07-20 23:34:42 +01:00
parent 0617ea5289
commit b329c8422d
6 changed files with 294 additions and 42 deletions

View File

@@ -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 (
<Layout>
@@ -537,6 +569,19 @@ export default function OrderDetailsPage() {
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setRefreshTrigger(prev => prev + 1)}
disabled={loading}
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin mr-1" />
) : (
<RefreshCw className="h-4 w-4 mr-1" />
)}
Refresh
</Button>
{prevOrderId && (
<Button
variant="outline"

View File

@@ -27,7 +27,6 @@ const ImportProductsModal = dynamic(() => import("@/components/modals/import-pro
loading: () => <div>Loading...</div>
});
// Loading skeleton for the product table
function ProductTableSkeleton() {
return (
<Card>

View File

@@ -13,6 +13,7 @@ import {
import { getCookie } from "@/lib/api";
import axios from "axios";
import { useRouter } from "next/navigation";
import { cacheUtils } from "@/lib/api-client";
interface Order {
_id: string;
@@ -46,6 +47,17 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
const lastFetchedRef = useRef<number>(0);
const isFetchingRef = useRef<boolean>(false);
const tooltipDelayRef = useRef<NodeJS.Timeout | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
// Add order refresh subscription
useEffect(() => {
const unsubscribe = cacheUtils.onOrderRefresh(() => {
console.log("Order refresh triggered in BuyerOrderInfo");
setRefreshTrigger(prev => prev + 1);
});
return unsubscribe;
}, []);
// Fetch data without unnecessary dependencies to reduce render cycles
const fetchBuyerOrders = useCallback(async (force = false) => {
@@ -57,7 +69,7 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
// Don't fetch if we already have orders and data was fetched less than 10 seconds ago
const now = Date.now();
if (!force && orders.length > 0 && now - lastFetchedRef.current < 10000) return;
if (!force && !refreshTrigger && orders.length > 0 && now - lastFetchedRef.current < 10000) return;
// Only continue if we have a chatId
if (!chatId) return;
@@ -69,8 +81,8 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
const authToken = getCookie("Authorization");
if (!authToken) {
isFetchingRef.current = false;
setLoading(false);
console.error("No auth token found for buyer orders");
setHasOrders(false);
return;
}
@@ -92,14 +104,22 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
}
lastFetchedRef.current = Date.now();
} catch (error) {
} catch (error: any) {
console.error("Error fetching buyer orders:", error);
if (error.response?.status === 404) {
console.log("No orders found for this buyer");
setOrders([]);
setHasOrders(false);
} else {
console.error("API error:", error.response?.data || error.message);
setHasOrders(null);
}
} finally {
setLoading(false);
isFetchingRef.current = false;
}
}, [chatId]); // Minimize dependencies even further
}, [chatId, refreshTrigger]); // Add refreshTrigger as dependency
// Start fetching immediately when component mounts
useEffect(() => {
@@ -145,9 +165,15 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
return `£${price.toFixed(2)}`;
};
// Helper function to check if order is underpaid
// Helper function to check if order is underpaid (improved)
const isOrderUnderpaid = (order: Order) => {
return order.underpaid === true && order.underpaymentAmount && order.underpaymentAmount > 0;
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 percentage
@@ -196,33 +222,27 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
)}
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
className="w-80 p-0"
>
<TooltipContent side="left" className="p-0 max-w-xs">
{loading ? (
<div className="p-4 flex items-center justify-center">
<Loader2 className="h-4 w-4 animate-spin mr-2" />
<div className="p-3 flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Loading orders...</span>
</div>
) : orders.length === 0 ? (
<div className="p-4 text-center">
<Package className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">No orders found</p>
<div className="p-3">
<span className="text-sm text-muted-foreground">No orders found</span>
</div>
) : (
<>
<div className="p-3 border-b bg-muted/50">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm">Recent Orders</h4>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Package className="h-3 w-3" />
{orders.length} {orders.length === 1 ? 'order' : 'orders'}
</div>
</div>
<div className="p-3 border-b">
<h4 className="font-medium text-sm">Customer Orders</h4>
<p className="text-xs text-muted-foreground">
{orders.length} {orders.length === 1 ? 'order' : 'orders'} Total: {formatPrice(
orders.reduce((sum, order) => sum + order.totalPrice, 0)
)}
</p>
</div>
{/* Show orders */}
<div className="max-h-64 overflow-y-auto divide-y divide-border">
{orders.map((order) => (
<div

View File

@@ -17,6 +17,7 @@ import { clientFetch } from "@/lib/api";
import { toast } from "sonner";
import { getCookie } from "@/lib/api";
import axios from "axios";
import { cacheUtils } from '@/lib/api-client';
interface Order {
_id: string;
@@ -24,6 +25,8 @@ interface Order {
status: string;
totalPrice: number;
orderDate: string;
underpaid?: boolean;
underpaymentAmount?: number;
}
interface ChatMessage {
@@ -139,15 +142,22 @@ export default function UnifiedNotifications() {
const orderData = await clientFetch(`/orders?status=paid&limit=10&orderDate[gte]=${timestamp}`);
const orders: Order[] = orderData.orders || [];
// Filter out orders that are still showing as underpaid (cache issue)
const validPaidOrders = orders.filter(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);

View File

@@ -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<number>(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 (
<div className="space-y-4">
<div className="border border-zinc-800 rounded-md bg-black/40 overflow-hidden">

View File

@@ -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<string, CacheEntry>();
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
@@ -262,3 +352,45 @@ export const getCustomers = async (page: number = 1, limit: number = 25): Promis
export const getCustomerDetails = async (userId: string): Promise<CustomerStats> => {
return clientFetch(`/customers/${userId}`);
};
/**
* Enhanced client-side fetch function with caching and automatic invalidation
*/
export async function clientFetchWithCache<T = any>(
url: string,
options: RequestInit = {},
cacheKey?: string,
ttl?: number
): Promise<T> {
// 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<T>(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;
}