diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx
index c4b6b75..9a7cc8c 100644
--- a/app/dashboard/admin/page.tsx
+++ b/app/dashboard/admin/page.tsx
@@ -1,16 +1,61 @@
+"use client";
export const dynamic = "force-dynamic";
-import React from "react";
-import AdminAnalytics from "@/components/admin/AdminAnalytics";
-import InviteVendorCard from "@/components/admin/InviteVendorCard";
-import BanUserCard from "@/components/admin/BanUserCard";
-import InvitationsListCard from "@/components/admin/InvitationsListCard";
-import VendorsCard from "@/components/admin/VendorsCard";
+import React, { Suspense, lazy, useState } from "react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+
+// Lazy load admin components
+const AdminAnalytics = lazy(() => import("@/components/admin/AdminAnalytics"));
+const InviteVendorCard = lazy(() => import("@/components/admin/InviteVendorCard"));
+const BanUserCard = lazy(() => import("@/components/admin/BanUserCard"));
+const InvitationsListCard = lazy(() => import("@/components/admin/InvitationsListCard"));
+const VendorsCard = lazy(() => import("@/components/admin/VendorsCard"));
+
+// Loading skeleton for admin components
+function AdminComponentSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Loading skeleton for management cards
+function ManagementCardsSkeleton() {
+ return (
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
export default function AdminPage() {
+ const [activeTab, setActiveTab] = useState("analytics");
+
return (
@@ -23,23 +68,27 @@ export default function AdminPage() {
-
+
Analytics
Management
-
+ }>
+
+
-
-
-
-
-
-
+ }>
+
+
+
+
+
+
+
diff --git a/components/admin/OrderDetailsModal.tsx b/components/admin/OrderDetailsModal.tsx
new file mode 100644
index 0000000..6b5d2bc
--- /dev/null
+++ b/components/admin/OrderDetailsModal.tsx
@@ -0,0 +1,553 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Button } from "@/components/ui/button";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { fetchClient } from "@/lib/api-client";
+import { toast } from "sonner";
+import { Package, User, Calendar, DollarSign, MapPin, Truck, CheckCircle, XCircle, Clock, Wallet, Copy, ExternalLink } from "lucide-react";
+
+interface OrderDetails {
+ _id: string;
+ orderId: number;
+ telegramBuyerId: string;
+ telegramUsername?: string;
+ totalPrice: number;
+ cryptoTotal?: number;
+ orderDate: string;
+ paidAt?: string;
+ status: string;
+ products: Array<{
+ productId: string;
+ name: string;
+ quantity: number;
+ pricePerUnit: number;
+ totalItemPrice: number;
+ }>;
+ shippingMethod?: {
+ type?: string;
+ name?: string; // Legacy support
+ price: number;
+ };
+ pgpAddress?: string;
+ trackingNumber?: string;
+ discountAmount?: number;
+ subtotalBeforeDiscount?: number;
+ paymentAddress?: string;
+ txid?: string | string[];
+ vendorId?: {
+ username?: string;
+ };
+ storeId?: string;
+}
+
+interface OrderDetailsModalProps {
+ orderId: number | string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+/**
+ * Admin-only order details modal component
+ * This component fetches order details from admin-only API endpoints
+ * and should only be used in admin contexts
+ */
+
+// Helper function to format currency, removing unnecessary .00
+const formatCurrency = (amount: number | undefined | null): string => {
+ if (amount === undefined || amount === null || isNaN(amount) || amount === 0) {
+ return '';
+ }
+ // If it's a whole number, don't show decimals
+ if (amount % 1 === 0) {
+ return `£${amount.toFixed(0)}`;
+ }
+ return `£${amount.toFixed(2)}`;
+};
+
+const getStatusConfig = (status: string) => {
+ switch (status) {
+ case 'acknowledged':
+ return { label: 'Acknowledged', color: 'bg-purple-500/10 text-purple-500 border-purple-500/20', icon: CheckCircle };
+ case 'paid':
+ return { label: 'Paid', color: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20', icon: CheckCircle };
+ case 'shipped':
+ return { label: 'Shipped', color: 'bg-blue-500/10 text-blue-500 border-blue-500/20', icon: Truck };
+ case 'completed':
+ return { label: 'Completed', color: 'bg-green-500/10 text-green-500 border-green-500/20', icon: CheckCircle };
+ case 'cancelled':
+ return { label: 'Cancelled', color: 'bg-red-500/10 text-red-500 border-red-500/20', icon: XCircle };
+ case 'unpaid':
+ return { label: 'Unpaid', color: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20', icon: Clock };
+ case 'confirming':
+ return { label: 'Confirming', color: 'bg-orange-500/10 text-orange-500 border-orange-500/20', icon: Clock };
+ default:
+ return { label: status, color: 'bg-gray-500/10 text-gray-500 border-gray-500/20', icon: Package };
+ }
+};
+
+export default function OrderDetailsModal({ orderId, open, onOpenChange }: OrderDetailsModalProps) {
+ const [order, setOrder] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [copied, setCopied] = useState(null);
+ const [updatingStatus, setUpdatingStatus] = useState(false);
+
+ const copyToClipboard = async (text: string, label: string) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(label);
+ setTimeout(() => setCopied(null), 2000);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ };
+
+ const handleStatusChange = async (newStatus: string) => {
+ if (!order || newStatus === order.status) return;
+
+ setUpdatingStatus(true);
+ try {
+ const response = await fetchClient<{ message: string; order: { orderId: number; status: string; oldStatus: string } }>(
+ `/admin/orders/${orderId}/status`,
+ {
+ method: 'PUT',
+ body: { status: newStatus }
+ }
+ );
+
+ // Update local order state
+ setOrder({ ...order, status: newStatus });
+
+ toast.success(`Order status updated to ${newStatus}`);
+
+ // Optionally refresh order details to get latest data
+ await fetchOrderDetails();
+ } catch (err) {
+ console.error('Failed to update order status:', err);
+ const errorMessage = err instanceof Error ? err.message : 'Failed to update order status';
+ toast.error(errorMessage);
+ } finally {
+ setUpdatingStatus(false);
+ }
+ };
+
+ useEffect(() => {
+ if (open && orderId) {
+ fetchOrderDetails();
+ } else {
+ // Reset state when modal closes
+ setOrder(null);
+ setError(null);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open, orderId]);
+
+ const fetchOrderDetails = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ // Fetch full order details from admin endpoint
+ // Use /admin/orders/:orderId (fetchClient will add /api prefix and backend URL)
+ console.log(`Fetching order details for order #${orderId}`);
+ const orderData = await fetchClient(`/admin/orders/${orderId}`);
+
+ console.log("Order data received:", orderData);
+
+ if (!orderData) {
+ throw new Error("No order data received from server");
+ }
+
+ if (!orderData.orderId) {
+ throw new Error("Invalid order data: missing orderId");
+ }
+
+ setOrder(orderData);
+ } catch (err) {
+ console.error("Failed to fetch order details:", err);
+ const errorMessage = err instanceof Error
+ ? err.message
+ : typeof err === 'string'
+ ? err
+ : "Failed to load order details. Please check your connection and try again.";
+ setError(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const statusConfig = order ? getStatusConfig(order.status) : null;
+ const StatusIcon = statusConfig?.icon || Package;
+
+ return (
+
+ );
+}
+
diff --git a/components/admin/OrdersTable.tsx b/components/admin/OrdersTable.tsx
index 2363d09..2a38a7b 100644
--- a/components/admin/OrdersTable.tsx
+++ b/components/admin/OrdersTable.tsx
@@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
+import OrderDetailsModal from "./OrderDetailsModal";
interface Order {
orderId: string | number;
@@ -23,6 +24,11 @@ interface Order {
interface OrdersTableProps {
orders: Order[];
+ /**
+ * Enable order details modal (admin-only feature)
+ * @default true
+ */
+ enableModal?: boolean;
}
const getStatusStyle = (status: string) => {
@@ -46,10 +52,16 @@ const getStatusStyle = (status: string) => {
}
};
-export default function OrdersTable({ orders }: OrdersTableProps) {
+/**
+ * Admin-only orders table component with order details modal
+ * This component should only be used in admin contexts
+ */
+export default function OrdersTable({ orders, enableModal = true }: OrdersTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
+ const [selectedOrderId, setSelectedOrderId] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
const itemsPerPage = 10;
// Filter orders based on search and status
@@ -83,6 +95,11 @@ export default function OrdersTable({ orders }: OrdersTableProps) {
setCurrentPage(1); // Reset to first page when filtering
};
+ const handleViewOrder = (orderId: number | string) => {
+ setSelectedOrderId(orderId);
+ setIsModalOpen(true);
+ };
+
return (
@@ -153,9 +170,25 @@ export default function OrdersTable({ orders }: OrdersTableProps) {
N/A
{new Date(order.createdAt).toLocaleDateString()}
-
+ {enableModal ? (
+
+ ) : (
+
+ )}
))}
@@ -209,6 +242,15 @@ export default function OrdersTable({ orders }: OrdersTableProps) {
)}
+
+ {/* Order Details Modal - Admin only */}
+ {enableModal && selectedOrderId && (
+
+ )}
);
}
diff --git a/lib/api-client.ts b/lib/api-client.ts
index a63bd85..c7e7781 100644
--- a/lib/api-client.ts
+++ b/lib/api-client.ts
@@ -282,7 +282,9 @@ export async function fetchClient(
}
} else {
// For direct API calls, construct full URL
- url = `${apiUrl}${normalizedEndpoint}`;
+ // Ensure /api prefix is included if apiUrl doesn't already have it
+ const baseUrl = apiUrl.endsWith('/api') ? apiUrl : `${apiUrl}/api`;
+ url = `${baseUrl}${normalizedEndpoint}`;
}
// Get auth token from cookies
diff --git a/public/git-info.json b/public/git-info.json
index 6db9608..e01af18 100644
--- a/public/git-info.json
+++ b/public/git-info.json
@@ -1,4 +1,4 @@
{
- "commitHash": "07dcaf5",
- "buildTime": "2025-12-15T17:54:38.040Z"
+ "commitHash": "93ec3d3",
+ "buildTime": "2025-12-17T23:21:50.682Z"
}
\ No newline at end of file