oh
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
setHasOrders(false);
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
@@ -409,11 +452,11 @@ export default function OrderTable() {
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span>£{order.totalPrice.toFixed(2)}</span>
|
||||
{underpaidInfo && (
|
||||
<span className="text-xs text-red-400">
|
||||
Missing: £{(underpaidInfo.missing * 100).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
{underpaidInfo && (
|
||||
<span className="text-xs text-red-400">
|
||||
Missing: £{(underpaidInfo.missing * 100).toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -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
|
||||
@@ -261,4 +351,46 @@ 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;
|
||||
}
|
||||
Reference in New Issue
Block a user