diff --git a/app/dashboard/orders/[id]/page.tsx b/app/dashboard/orders/[id]/page.tsx index 2b4cb70..2ebd044 100644 --- a/app/dashboard/orders/[id]/page.tsx +++ b/app/dashboard/orders/[id]/page.tsx @@ -39,6 +39,8 @@ import { } from "@/components/ui/alert-dialog"; import Layout from "@/components/layout/layout"; import { cacheUtils } from '@/lib/api-client'; +import OrderTimeline from "@/components/orders/order-timeline"; +import { motion, AnimatePresence } from "framer-motion"; interface Order { orderId: string; @@ -170,7 +172,7 @@ export default function OrderDetailsPage() { authToken: string ): Promise> => { const productNamesMap: Record = {}; - + // Process each product ID independently const fetchPromises = productIds.map(async (id) => { try { @@ -184,10 +186,10 @@ export default function OrderDetailsPage() { productNamesMap[id] = "Unknown Product (Deleted)"; } }); - + // Wait for all fetch operations to complete (successful or failed) await Promise.all(fetchPromises); - + return productNamesMap; }; @@ -195,38 +197,38 @@ export default function OrderDetailsPage() { try { // Add a loading state to give feedback const loadingToast = toast.loading("Marking order as paid..."); - + // Log the request for debugging console.log(`Sending request to /orders/${orderId}/status with clientFetch`); console.log("Request payload:", { status: "paid" }); - + // Use clientFetch which handles API URL and auth token automatically const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", body: JSON.stringify({ status: "paid" }), }); - + // Log the response console.log("API response:", response); toast.dismiss(loadingToast); - + if (response && response.message === "Order status updated successfully") { // Update both states setIsPaid(true); - setOrder((prevOrder) => (prevOrder ? { - ...prevOrder, + 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); @@ -240,14 +242,14 @@ export default function OrderDetailsPage() { } } catch (error: any) { console.error("Failed to mark order as paid:", error); - + // More detailed error handling let errorMessage = "Failed to mark order as paid"; - + if (error.message) { errorMessage += `: ${error.message}`; } - + if (error.response) { try { const errorData = await error.response.json(); @@ -259,7 +261,7 @@ export default function OrderDetailsPage() { console.error("Could not parse error response:", e); } } - + toast.error(errorMessage); } }; @@ -317,7 +319,7 @@ export default function OrderDetailsPage() { ...prevOrder, trackingNumber: trackingNumber } : null); - + toast.success("Tracking number added successfully!"); } catch (err: any) { console.error("Failed to add tracking number:", err); @@ -330,7 +332,7 @@ export default function OrderDetailsPage() { const handleMarkAsAcknowledged = async () => { try { setIsAcknowledging(true); - + // Use clientFetch which handles API URL and auth token automatically const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", @@ -382,7 +384,7 @@ export default function OrderDetailsPage() { const handleCancelOrder = async () => { try { setIsCancelling(true); - + // Use clientFetch which handles API URL and auth token automatically const response = await clientFetch(`/orders/${orderId}/status`, { method: "PUT", @@ -429,10 +431,10 @@ export default function OrderDetailsPage() { const productIds = data.order.products.map((product) => product.productId); const productNamesMap = await fetchProductNames(productIds, authToken); setProductNames(productNamesMap); - + setTimeout(() => { setProductNames(prev => { - const newMap = {...prev}; + const newMap = { ...prev }; productIds.forEach(id => { if (!newMap[id] || newMap[id] === "Loading...") { newMap[id] = "Unknown Product (Deleted)"; @@ -440,7 +442,7 @@ export default function OrderDetailsPage() { }); return newMap; }); - }, 3000); + }, 3000); if (data.order.status === "paid") { setIsPaid(true); @@ -460,7 +462,7 @@ export default function OrderDetailsPage() { const fetchAdjacentOrders = async () => { try { const authToken = document.cookie.split("Authorization=")[1]; - + if (!order?.orderId) return; // Get the current numerical orderId @@ -470,7 +472,7 @@ export default function OrderDetailsPage() { // Use the new optimized backend endpoint to get adjacent orders const adjacentOrdersUrl = `${process.env.NEXT_PUBLIC_API_URL}/orders/adjacent/${currentOrderId}`; console.log('Fetching adjacent orders:', adjacentOrdersUrl); - + const adjacentOrdersRes = await fetchData( adjacentOrdersUrl, { @@ -488,17 +490,17 @@ export default function OrderDetailsPage() { // Set the next and previous order IDs const { newer, older } = adjacentOrdersRes; - + // Set IDs for navigation setPrevOrderId(newer?._id || null); setNextOrderId(older?._id || null); - + if (newer) { console.log(`Newer order: ${newer.orderId} (ID: ${newer._id})`); } else { console.log('No newer order found'); } - + if (older) { console.log(`Older order: ${older.orderId} (ID: ${older._id})`); } else { @@ -544,7 +546,7 @@ export default function OrderDetailsPage() { ...prevOrder, trackingNumber: trackingNumber } : null); - + toast.success("Tracking number updated successfully!"); setTrackingNumber(""); // Clear the input } catch (err: any) { @@ -569,11 +571,11 @@ export default function OrderDetailsPage() { try { const lines = []; - + // Order number lines.push(`Order Number: ${order.orderId}`); lines.push(''); - + // Order details lines.push('Order Details:'); if (order.products && order.products.length > 0) { @@ -582,30 +584,30 @@ export default function OrderDetailsPage() { lines.push(` - ${productName} (Qty: ${product.quantity} @ £${product.pricePerUnit.toFixed(2)} = £${product.totalItemPrice.toFixed(2)})`); }); } - + // Shipping if (order.shippingMethod) { lines.push(` - Shipping: ${order.shippingMethod.type} (£${order.shippingMethod.price.toFixed(2)})`); } - + // Discount if (order.discountAmount && order.discountAmount > 0) { lines.push(` - Discount: -£${order.discountAmount.toFixed(2)}${order.promotionCode ? ` (Promo: ${order.promotionCode})` : ''}`); } - + // Subtotal if different from total if (order.subtotalBeforeDiscount && order.subtotalBeforeDiscount !== order.totalPrice) { lines.push(` - Subtotal: £${order.subtotalBeforeDiscount.toFixed(2)}`); } - + // Total lines.push(` - Total: £${order.totalPrice.toFixed(2)}`); lines.push(''); - + // Address lines.push('Address:'); lines.push(order.pgpAddress || 'N/A'); - + const textToCopy = lines.join('\n'); await navigator.clipboard.writeText(textToCopy); toast.success("Order data copied to clipboard!"); @@ -618,28 +620,28 @@ export default function OrderDetailsPage() { // Helper function to check if order is underpaid const isOrderUnderpaid = (order: Order | null) => { // 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"; + return order?.underpaid === true && + order?.underpaymentAmount && + order.underpaymentAmount > 0 && + order.status !== "paid" && + order.status !== "completed" && + order.status !== "shipped"; }; // Helper function to get underpaid information const getUnderpaidInfo = (order: Order | null) => { if (!isOrderUnderpaid(order)) return null; - + const received = order?.lastBalanceReceived || 0; const required = order?.cryptoTotal || 0; const missing = order?.underpaymentAmount || 0; - + // Calculate LTC to GBP exchange rate from order data const ltcToGbpRate = required > 0 ? (order?.totalPrice || 0) / required : 0; const receivedGbp = received * ltcToGbpRate; const requiredGbp = order?.totalPrice || 0; const missingGbp = missing * ltcToGbpRate; - + return { received, required, @@ -772,7 +774,7 @@ export default function OrderDetailsPage() {

{underpaidInfo.percentage}% paid

- + {order?.paymentAddress && (

Payment Address:

@@ -791,10 +793,26 @@ export default function OrderDetailsPage() { )} + {/* Order Timeline */} + + + Order Lifecycle + + + + + - - -
+ {/* Left Column - Order Details */}
{/* Products Card */} @@ -929,7 +947,7 @@ export default function OrderDetailsPage() {
Customer Since - {customerInsights.firstOrder ? + {customerInsights.firstOrder ? new Date(customerInsights.firstOrder).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', @@ -1092,34 +1110,34 @@ export default function OrderDetailsPage() { )} {/* Cancel Order Button */} - {order?.status !== "cancelled" && - order?.status !== "completed" && - order?.status !== "shipped" && ( - - - - - - - Cancel Order - - Are you sure you want to cancel this order? This action cannot be undone. - - - - Cancel - - Confirm Cancel - - - - - )} + {order?.status !== "cancelled" && + order?.status !== "completed" && + order?.status !== "shipped" && ( + + + + + + + Cancel Order + + Are you sure you want to cancel this order? This action cannot be undone. + + + + Cancel + + Confirm Cancel + + + + + )} {/* No Actions Available Message */} {(order?.status === "completed" || order?.status === "cancelled") && ( @@ -1168,11 +1186,10 @@ export default function OrderDetailsPage() { {[...Array(5)].map((_, i) => ( @@ -1198,10 +1215,10 @@ export default function OrderDetailsPage() { )}
-
- - {/* Shipping Dialog removed; use inline tracking input above */} +
+ + {/* Shipping Dialog removed; use inline tracking input above */} ); -} \ No newline at end of file +} diff --git a/app/dashboard/storefront/customers/page.tsx b/app/dashboard/storefront/customers/page.tsx index 9841853..f4099c3 100644 --- a/app/dashboard/storefront/customers/page.tsx +++ b/app/dashboard/storefront/customers/page.tsx @@ -30,27 +30,32 @@ import { DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { - ChevronLeft, - ChevronRight, - Loader2, - Users, +import { + ChevronLeft, + ChevronRight, + Loader2, + Users, ArrowUpDown, MessageCircle, UserPlus, MoreHorizontal, Search, - X + X, + CreditCard, + Calendar, + ShoppingBag } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { motion, AnimatePresence } from "framer-motion"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, -} from "@/components/ui/dropdown-menu"; +} from "@/components/ui/dropdown-menu"; export default function CustomerManagementPage() { const router = useRouter(); @@ -71,32 +76,32 @@ export default function CustomerManagementPage() { try { setLoading(true); const response = await getCustomers(page, itemsPerPage); - + // Sort customers based on current sort config let sortedCustomers = [...response.customers]; sortedCustomers.sort((a, b) => { if (sortConfig.column === "totalOrders") { - return sortConfig.direction === "asc" - ? a.totalOrders - b.totalOrders + return sortConfig.direction === "asc" + ? a.totalOrders - b.totalOrders : b.totalOrders - a.totalOrders; } else if (sortConfig.column === "totalSpent") { - return sortConfig.direction === "asc" - ? a.totalSpent - b.totalSpent + return sortConfig.direction === "asc" + ? a.totalSpent - b.totalSpent : b.totalSpent - a.totalSpent; } else if (sortConfig.column === "lastOrderDate") { // Handle null lastOrderDate values if (!a.lastOrderDate && !b.lastOrderDate) return 0; if (!a.lastOrderDate) return sortConfig.direction === "asc" ? -1 : 1; if (!b.lastOrderDate) return sortConfig.direction === "asc" ? 1 : -1; - + // Both have valid dates - return sortConfig.direction === "asc" - ? new Date(a.lastOrderDate).getTime() - new Date(b.lastOrderDate).getTime() + return sortConfig.direction === "asc" + ? new Date(a.lastOrderDate).getTime() - new Date(b.lastOrderDate).getTime() : new Date(b.lastOrderDate).getTime() - new Date(a.lastOrderDate).getTime(); } return 0; }); - + setCustomers(sortedCustomers); setFilteredCustomers(sortedCustomers); setTotalPages(Math.ceil(response.total / itemsPerPage)); @@ -138,424 +143,444 @@ export default function CustomerManagementPage() { } }, [searchQuery, customers]); - const handlePageChange = (newPage: number) => { - setPage(newPage); - }; + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; - const handleItemsPerPageChange = (value: string) => { - setItemsPerPage(parseInt(value, 10)); - setPage(1); - }; + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(parseInt(value, 10)); + setPage(1); + }; - const handleSort = (column: "totalOrders" | "totalSpent" | "lastOrderDate") => { - setSortConfig(prev => ({ - column, - direction: prev.column === column && prev.direction === "asc" ? "desc" : "asc" - })); - }; + const handleSort = (column: "totalOrders" | "totalSpent" | "lastOrderDate") => { + setSortConfig(prev => ({ + column, + direction: prev.column === column && prev.direction === "asc" ? "desc" : "asc" + })); + }; - const clearSearch = () => { - setSearchQuery(""); - }; + const clearSearch = () => { + setSearchQuery(""); + }; - const formatDate = (dateString: string | null | undefined) => { - if (!dateString) return "N/A"; - try { - const date = new Date(dateString); - return new Intl.DateTimeFormat('en-GB', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(date); - } catch (error) { - return "N/A"; - } - }; + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return "N/A"; + try { + const date = new Date(dateString); + return new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }).format(date); + } catch (error) { + return "N/A"; + } + }; - return ( - -
-
-

- - Customer Management -

+ return ( + +
+
+

+ + Customer Management +

+
+ + +
+
+
+
Show:
+ +
-
-
-
-
-
Show:
- -
-
- -
-
- -
- setSearchQuery(e.target.value)} - className="pl-10 pr-10 py-2 w-full bg-black/40 border-zinc-700 text-white" - /> - {searchQuery && ( - - )} -
- -
- {loading - ? "Loading..." - : searchQuery - ? `Found ${filteredCustomers.length} matching customers` - : `Showing ${filteredCustomers.length} of ${totalPages * itemsPerPage} customers`} -
+
+
+
+ setSearchQuery(e.target.value)} + className="pl-10 pr-10 py-2 w-full bg-background/50 border-border/50 focus:ring-primary/20 transition-all duration-300" + /> + {searchQuery && ( + + )} +
- {loading ? ( -
- {/* Loading indicator */} -
-
-
- - {/* Table skeleton */} -
-
- {['Customer', 'Orders', 'Total Spent', 'Last Order', 'Status'].map((header, i) => ( - - ))} -
- - {[...Array(5)].map((_, i) => ( -
-
- -
- - -
-
- - - - -
+
+ {loading + ? "Loading..." + : searchQuery + ? `Found ${filteredCustomers.length} matching customers` + : `Showing ${filteredCustomers.length} of ${totalPages * itemsPerPage} customers`} +
+
+ + + {loading ? ( +
+ {/* Loading indicator */} +
+
+
+ +
+
+ {['Customer', 'Orders', 'Total Spent', 'Last Order', 'Status'].map((header, i) => ( + ))}
+ + {[...Array(5)].map((_, i) => ( +
+
+ +
+ + +
+
+ + + + +
+ ))}
- ) : filteredCustomers.length === 0 ? ( -
- -

- {searchQuery ? "No customers matching your search" : "No customers found"} -

-

- {searchQuery - ? "Try a different search term or clear the search" - : "Once you have customers placing orders, they will appear here."} -

- {searchQuery && ( - - )} +
+ ) : filteredCustomers.length === 0 ? ( +
+
+
- ) : ( -
- - - - Customer - handleSort("totalOrders")} - > -
- Orders - -
-
- handleSort("totalSpent")} - > -
- Total Spent - -
-
- handleSort("lastOrderDate")} - > -
- Last Order - -
-
- Status -
-
- - {filteredCustomers.map((customer) => ( - + {searchQuery ? "No matching customers" : "No customers yet"} + +

+ {searchQuery + ? "We couldn't find any customers matching your search criteria." + : "Once you have customers placing orders, they will appear here."} +

+ {searchQuery && ( + + )} + + ) : ( +
+
+ + + Customer + handleSort("totalOrders")} + > +
+ Orders + +
+
+ handleSort("totalSpent")} + > +
+ Total Spent + +
+
+ handleSort("lastOrderDate")} + > +
+ Last Order + +
+
+ Status +
+
+ + + {filteredCustomers.map((customer, index) => ( + setSelectedCustomer(customer)} > - -
- @{customer.telegramUsername || "Unknown"} - {!customer.hasOrders && ( - - - New - - )} + +
+
+ {customer.telegramUsername ? customer.telegramUsername.substring(0, 2).toUpperCase() : 'ID'} +
+
+
+ @{customer.telegramUsername || "Unknown"} + {!customer.hasOrders && ( + + New + + )} +
+
+ ID: + {customer.telegramUserId} +
+
-
ID: {customer.telegramUserId}
- {customer.totalOrders} + + {customer.totalOrders} + - + {formatCurrency(customer.totalSpent)} - - {customer.lastOrderDate ? formatDate(customer.lastOrderDate) : "N/A"} + + {customer.lastOrderDate ? ( +
+ + {formatDate(customer.lastOrderDate).split(",")[0]} +
+ ) : "Never"}
{customer.hasOrders ? ( -
- - {customer.ordersByStatus.paid} Paid - - - {customer.ordersByStatus.completed} Completed - - - {customer.ordersByStatus.shipped} Shipped - +
+ {customer.ordersByStatus.paid > 0 && ( + + {customer.ordersByStatus.paid} Paid + + )} + {customer.ordersByStatus.completed > 0 && ( + + {customer.ordersByStatus.completed} Done + + )} + {customer.ordersByStatus.shipped > 0 && ( + + {customer.ordersByStatus.shipped} Ship + + )}
) : ( - - No orders yet - + No activity )} - + ))} - -
-
- )} + + + +
+ )} + -
-
- Page {page} of {totalPages} +
+
+ Page {page} of {totalPages} +
+
+ + + {totalPages > 2 ? ( + + + + + + {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => ( + handlePageChange(pageNum)} + className={pageNum === page ? 'bg-primary/10 text-primary' : ''} + > + Page {pageNum} + + ))} + + + ) : null} + + +
+
+ + + {/* Customer Details Dialog */} + {selectedCustomer && ( + !open && setSelectedCustomer(null)}> + + + + Customer Details + + + +
+ {/* Customer Information */} +
+
+

Customer Information

+
+
+
Username:
+
@{selectedCustomer.telegramUsername || "Unknown"}
+
+
+
Telegram ID:
+
{selectedCustomer.telegramUserId}
+
+
+
Chat ID:
+
{selectedCustomer.chatId}
+
+
+
+ +
-
- - - {totalPages > 2 && ( - - - - - - {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => ( - handlePageChange(pageNum)} - className={`cursor-pointer ${pageNum === page ? 'bg-zinc-800 text-white' : 'text-gray-300'}`} - > - Page {pageNum} - - ))} - - - )} - - + + {/* Order Statistics */} +
+

Order Statistics

+
+
+
Total Orders:
+
{selectedCustomer.totalOrders}
+
+
+
Total Spent:
+
{formatCurrency(selectedCustomer.totalSpent)}
+
+
+
First Order:
+
+ {formatDate(selectedCustomer.firstOrderDate)} +
+
+
+
Last Order:
+
+ {formatDate(selectedCustomer.lastOrderDate)} +
+
+
-
- {/* Customer Details Dialog */} - {selectedCustomer && ( - !open && setSelectedCustomer(null)}> - - - - Customer Details - - - -
- {/* Customer Information */} -
-
-

Customer Information

-
-
-
Username:
-
@{selectedCustomer.telegramUsername || "Unknown"}
-
-
-
Telegram ID:
-
{selectedCustomer.telegramUserId}
-
-
-
Chat ID:
-
{selectedCustomer.chatId}
-
-
-
- - -
- - {/* Order Statistics */} -
-

Order Statistics

-
-
-
Total Orders:
-
{selectedCustomer.totalOrders}
-
-
-
Total Spent:
-
{formatCurrency(selectedCustomer.totalSpent)}
-
-
-
First Order:
-
- {formatDate(selectedCustomer.firstOrderDate)} -
-
-
-
Last Order:
-
- {formatDate(selectedCustomer.lastOrderDate)} -
-
-
-
+ {/* Order Status Breakdown */} +
+

Order Status Breakdown

+
+
+

Paid

+

{selectedCustomer.ordersByStatus.paid}

- - {/* Order Status Breakdown */} -
-

Order Status Breakdown

-
-
-

Paid

-

{selectedCustomer.ordersByStatus.paid}

-
-
-

Acknowledged

-

{selectedCustomer.ordersByStatus.acknowledged}

-
-
-

Shipped

-

{selectedCustomer.ordersByStatus.shipped}

-
-
-

Completed

-

{selectedCustomer.ordersByStatus.completed}

-
-
+
+

Acknowledged

+

{selectedCustomer.ordersByStatus.acknowledged}

+
+

Shipped

+

{selectedCustomer.ordersByStatus.shipped}

+
+
+

Completed

+

{selectedCustomer.ordersByStatus.completed}

+
+
+
- - - - - -
- )} -
- - ); - } \ No newline at end of file + + + + + + + )} +
+ + ); +} \ No newline at end of file diff --git a/app/dashboard/storefront/page.tsx b/app/dashboard/storefront/page.tsx index 6eb9bda..471769d 100644 --- a/app/dashboard/storefront/page.tsx +++ b/app/dashboard/storefront/page.tsx @@ -6,11 +6,15 @@ import Layout from "@/components/layout/layout"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet } from "lucide-react"; +import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet, RefreshCw } from "lucide-react"; import { apiRequest } from "@/lib/api"; import { toast } from "sonner"; import BroadcastDialog from "@/components/modals/broadcast-dialog"; import Dashboard from "@/components/dashboard/dashboard"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { motion, AnimatePresence } from "framer-motion"; import { Select, SelectContent, @@ -166,251 +170,298 @@ export default function StorefrontPage() { return (
-
-
-

- - Storefront Settings -

-
- - - -
- - setStorefront((prev) => ({ - ...prev, - isEnabled: checked, - })) - } - /> - - {storefront.isEnabled ? 'Store Open' : 'Store Closed'} - -
-
- -

{storefront.isEnabled ? 'Click to close store' : 'Click to open store'}

-
-
-
+
+
+
+ +
+
+

+ Storefront Settings +

+

+ Manage your shop's appearance, policies, and configuration +

-
+
-
-
- {/* Security Settings */} -
-
-
- -

- Security -

-
-
-
- -