Enhance dashboard UI and add order timeline
All checks were successful
Build Frontend / build (push) Successful in 1m12s
All checks were successful
Build Frontend / build (push) Successful in 1m12s
Refactored dashboard pages for improved layout and visual consistency using Card components, motion animations, and updated color schemes. Added an OrderTimeline component to the order details page to visualize order lifecycle. Improved customer management page with better sorting, searching, and a detailed customer dialog. Updated storefront settings page with a modernized layout and clearer sectioning.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -167,7 +169,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);
|
||||
@@ -244,11 +246,11 @@ export default function OrderTable() {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ orderIds: Array.from(selectedOrders) })
|
||||
});
|
||||
|
||||
|
||||
// Only update orders that were successfully marked as shipped
|
||||
if (response.success && response.success.orders) {
|
||||
const successfulOrderIds = new Set(response.success.orders.map((o: any) => o.id));
|
||||
|
||||
|
||||
setOrders(prev =>
|
||||
prev.map(order =>
|
||||
successfulOrderIds.has(order._id)
|
||||
@@ -256,16 +258,16 @@ export default function OrderTable() {
|
||||
: order
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
if (response.failed && response.failed.count > 0) {
|
||||
toast.warning(`${response.failed.count} orders could not be marked as shipped`);
|
||||
}
|
||||
|
||||
|
||||
if (response.success.count > 0) {
|
||||
toast.success(`${response.success.count} orders marked as shipped`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setSelectedOrders(new Set());
|
||||
} catch (error) {
|
||||
toast.error("Failed to update orders");
|
||||
@@ -276,68 +278,69 @@ export default function OrderTable() {
|
||||
};
|
||||
|
||||
const statusConfig: Record<OrderStatus, StatusConfig> = {
|
||||
acknowledged: {
|
||||
icon: CheckCircle2,
|
||||
color: "text-white",
|
||||
bgColor: "bg-purple-600"
|
||||
acknowledged: {
|
||||
icon: CheckCircle2,
|
||||
color: "text-purple-100",
|
||||
bgColor: "bg-purple-600/90 shadow-[0_0_10px_rgba(147,51,234,0.3)]"
|
||||
},
|
||||
paid: {
|
||||
icon: CheckCircle2,
|
||||
color: "text-white",
|
||||
bgColor: "bg-emerald-600"
|
||||
paid: {
|
||||
icon: CheckCircle2,
|
||||
color: "text-emerald-100",
|
||||
bgColor: "bg-emerald-600/90 shadow-[0_0_10px_rgba(16,185,129,0.3)]",
|
||||
animate: "animate-pulse"
|
||||
},
|
||||
unpaid: {
|
||||
icon: XCircle,
|
||||
color: "text-white",
|
||||
bgColor: "bg-red-500"
|
||||
unpaid: {
|
||||
icon: XCircle,
|
||||
color: "text-amber-100",
|
||||
bgColor: "bg-amber-500/90"
|
||||
},
|
||||
confirming: {
|
||||
icon: Loader2,
|
||||
color: "text-white",
|
||||
bgColor: "bg-yellow-500",
|
||||
animate: "animate-spin"
|
||||
confirming: {
|
||||
icon: Loader2,
|
||||
color: "text-blue-100",
|
||||
bgColor: "bg-blue-500/90",
|
||||
animate: "animate-spin"
|
||||
},
|
||||
shipped: {
|
||||
icon: Truck,
|
||||
color: "text-white",
|
||||
bgColor: "bg-blue-600"
|
||||
shipped: {
|
||||
icon: Truck,
|
||||
color: "text-indigo-100",
|
||||
bgColor: "bg-indigo-600/90 shadow-[0_0_10px_rgba(79,70,229,0.3)]"
|
||||
},
|
||||
completed: {
|
||||
icon: CheckCircle2,
|
||||
color: "text-white",
|
||||
bgColor: "bg-green-600"
|
||||
completed: {
|
||||
icon: CheckCircle2,
|
||||
color: "text-green-100",
|
||||
bgColor: "bg-green-600/90"
|
||||
},
|
||||
cancelled: {
|
||||
icon: XCircle,
|
||||
color: "text-white",
|
||||
bgColor: "bg-gray-500"
|
||||
cancelled: {
|
||||
icon: XCircle,
|
||||
color: "text-gray-100",
|
||||
bgColor: "bg-gray-600/90"
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to determine if order is underpaid
|
||||
const isOrderUnderpaid = (order: Order) => {
|
||||
// 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";
|
||||
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
|
||||
const getUnderpaidInfo = (order: Order) => {
|
||||
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 / required : 0;
|
||||
const missingGbp = missing * ltcToGbpRate;
|
||||
|
||||
|
||||
return {
|
||||
received,
|
||||
required,
|
||||
@@ -377,7 +380,7 @@ export default function OrderTable() {
|
||||
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(() => {
|
||||
@@ -391,16 +394,16 @@ export default function OrderTable() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="border border-zinc-800 rounded-md bg-black/40 overflow-hidden">
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
||||
{/* Filters header */}
|
||||
<div className="p-4 border-b border-zinc-800 bg-black/60">
|
||||
<div className="p-4 border-b border-border/50 bg-muted/30">
|
||||
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:items-center w-full lg:w-auto">
|
||||
<StatusFilter
|
||||
currentStatus={statusFilter}
|
||||
onChange={setStatusFilter}
|
||||
/>
|
||||
|
||||
|
||||
<PageSizeSelector
|
||||
currentSize={itemsPerPage}
|
||||
onChange={(value) => handleItemsPerPageChange({ target: { value } } as React.ChangeEvent<HTMLSelectElement>)}
|
||||
@@ -413,6 +416,7 @@ export default function OrderTable() {
|
||||
disabled={exporting}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-background/50 border-border/50"
|
||||
>
|
||||
{exporting ? (
|
||||
<>
|
||||
@@ -428,12 +432,12 @@ export default function OrderTable() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2 self-end lg:self-auto">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button disabled={selectedOrders.size === 0 || isShipping}>
|
||||
<Truck className="mr-2 h-5 w-5" />
|
||||
<Button disabled={selectedOrders.size === 0 || isShipping} className="shadow-md">
|
||||
<Truck className="mr-2 h-4 w-4" />
|
||||
{isShipping ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
@@ -445,7 +449,7 @@ export default function OrderTable() {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Mark Orders as Shipped</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to mark {selectedOrders.size} order{selectedOrders.size !== 1 ? 's' : ''} as shipped?
|
||||
Are you sure you want to mark {selectedOrders.size} order{selectedOrders.size !== 1 ? 's' : ''} as shipped?
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
@@ -462,163 +466,168 @@ export default function OrderTable() {
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="relative">
|
||||
<CardContent className="p-0 relative min-h-[400px]">
|
||||
{loading && (
|
||||
<div className="absolute inset-0 bg-background/50 flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
<div className="absolute inset-0 bg-background/50 backdrop-blur-[1px] flex items-center justify-center z-50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-[calc(100vh-300px)] overflow-auto">
|
||||
<Table className="[&_tr]:border-b [&_tr]:border-zinc-800 [&_tr:last-child]:border-b-0 [&_td]:border-r [&_td]:border-zinc-800 [&_td:last-child]:border-r-0 [&_th]:border-r [&_th]:border-zinc-800 [&_th:last-child]:border-r-0 [&_tr:hover]:bg-zinc-900/70">
|
||||
<TableHeader className="bg-black/60 sticky top-0 z-10">
|
||||
<TableRow>
|
||||
<div className="max-h-[calc(100vh-350px)] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/50 sticky top-0 z-20">
|
||||
<TableRow className="hover:bg-transparent border-border/50">
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={selectedOrders.size === paginatedOrders.length && paginatedOrders.length > 0}
|
||||
onCheckedChange={toggleAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer" onClick={() => handleSort("orderId")}>
|
||||
Order ID <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||
<TableHead className="cursor-pointer hover:text-primary transition-colors" onClick={() => handleSort("orderId")}>
|
||||
Order ID <ArrowUpDown className="ml-2 inline h-3 w-3" />
|
||||
</TableHead>
|
||||
<TableHead className="cursor-pointer" onClick={() => handleSort("totalPrice")}>
|
||||
Total <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||
<TableHead className="cursor-pointer hover:text-primary transition-colors" onClick={() => handleSort("totalPrice")}>
|
||||
Total <ArrowUpDown className="ml-2 inline h-3 w-3" />
|
||||
</TableHead>
|
||||
<TableHead className="hidden lg:table-cell">Promotion</TableHead>
|
||||
<TableHead className="cursor-pointer" onClick={() => handleSort("status")}>
|
||||
Status <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||
<TableHead className="cursor-pointer hover:text-primary transition-colors" onClick={() => handleSort("status")}>
|
||||
Status <ArrowUpDown className="ml-2 inline h-3 w-3" />
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell cursor-pointer" onClick={() => handleSort("orderDate")}>
|
||||
Order Date <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||
<TableHead className="hidden md:table-cell cursor-pointer hover:text-primary transition-colors" onClick={() => handleSort("orderDate")}>
|
||||
Date <ArrowUpDown className="ml-2 inline h-3 w-3" />
|
||||
</TableHead>
|
||||
<TableHead className="hidden xl:table-cell cursor-pointer" onClick={() => handleSort("paidAt")}>
|
||||
Paid At <ArrowUpDown className="ml-2 inline h-4 w-4" />
|
||||
<TableHead className="hidden xl:table-cell cursor-pointer hover:text-primary transition-colors" onClick={() => handleSort("paidAt")}>
|
||||
Paid At <ArrowUpDown className="ml-2 inline h-3 w-3" />
|
||||
</TableHead>
|
||||
<TableHead className="hidden lg:table-cell">Buyer</TableHead>
|
||||
<TableHead className="w-24 text-center">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedOrders.map((order) => {
|
||||
const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle;
|
||||
const underpaidInfo = getUnderpaidInfo(order);
|
||||
<AnimatePresence mode="popLayout">
|
||||
{paginatedOrders.map((order, index) => {
|
||||
const StatusIcon = statusConfig[order.status as keyof typeof statusConfig]?.icon || XCircle;
|
||||
const underpaidInfo = getUnderpaidInfo(order);
|
||||
|
||||
return (
|
||||
<TableRow key={order._id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedOrders.has(order._id)}
|
||||
onCheckedChange={() => toggleSelection(order._id)}
|
||||
disabled={order.status !== "paid" && order.status !== "acknowledged"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>#{order.orderId}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span>£{order.totalPrice.toFixed(2)}</span>
|
||||
{underpaidInfo && (
|
||||
<span className="text-xs text-red-400">
|
||||
Missing: £{underpaidInfo.missingGbp.toFixed(2)} ({underpaidInfo.missing.toFixed(8)} LTC)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
{order.promotionCode ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tag className="h-3 w-3 text-green-500" />
|
||||
<span className="text-xs font-mono bg-green-100 text-green-800 px-2 py-0.5 rounded">
|
||||
{order.promotionCode}
|
||||
return (
|
||||
<motion.tr
|
||||
key={order._id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.03 }}
|
||||
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedOrders.has(order._id)}
|
||||
onCheckedChange={() => toggleSelection(order._id)}
|
||||
disabled={order.status !== "paid" && order.status !== "acknowledged"}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm font-medium">#{order.orderId}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">£{order.totalPrice.toFixed(2)}</span>
|
||||
{underpaidInfo && (
|
||||
<span className="text-[10px] text-destructive flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
-£{underpaidInfo.missingGbp.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-xs text-green-600">
|
||||
<Percent className="h-3 w-3" />
|
||||
<span>-£{(order.discountAmount || 0).toFixed(2)}</span>
|
||||
{order.subtotalBeforeDiscount && order.subtotalBeforeDiscount > 0 && (
|
||||
<span className="text-muted-foreground">
|
||||
(was £{order.subtotalBeforeDiscount.toFixed(2)})
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
{order.promotionCode ? (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tag className="h-3 w-3 text-emerald-500" />
|
||||
<span className="text-[10px] font-mono bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-1.5 py-0.5 rounded border border-emerald-500/20">
|
||||
{order.promotionCode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm ${
|
||||
statusConfig[order.status as OrderStatus]?.bgColor || "bg-gray-500"
|
||||
} ${statusConfig[order.status as OrderStatus]?.color || "text-white"}`}>
|
||||
{React.createElement(statusConfig[order.status as OrderStatus]?.icon || XCircle, {
|
||||
className: `h-4 w-4 ${statusConfig[order.status as OrderStatus]?.animate || ""}`
|
||||
})}
|
||||
{order.status.charAt(0).toUpperCase() + order.status.slice(1)}
|
||||
</div>
|
||||
{isOrderUnderpaid(order) && (
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-red-600 text-white">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{underpaidInfo?.percentage}%
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] text-emerald-600/80">
|
||||
<Percent className="h-2.5 w-2.5" />
|
||||
<span>-£{(order.discountAmount || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
{new Date(order.orderDate).toLocaleDateString("en-GB", {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})}
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-cell">
|
||||
{order.paidAt ? new Date(order.paidAt).toLocaleDateString("en-GB", {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}) : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
{order.telegramUsername ? `@${order.telegramUsername}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href={`/dashboard/orders/${order._id}`}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{(order.telegramBuyerId || order.telegramUsername) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
title={`Chat with customer${order.telegramUsername ? ` @${order.telegramUsername}` : ''}`}
|
||||
>
|
||||
<Link href={`/dashboard/chats/new?buyerId=${order.telegramBuyerId || order.telegramUsername}`}>
|
||||
<MessageCircle className="h-4 w-4 text-primary" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border shadow-sm ${statusConfig[order.status as OrderStatus]?.bgColor || "bg-muted text-muted-foreground border-border"} ${statusConfig[order.status as OrderStatus]?.color || ""}`}>
|
||||
{React.createElement(statusConfig[order.status as OrderStatus]?.icon || XCircle, {
|
||||
className: `h-3.5 w-3.5 ${statusConfig[order.status as OrderStatus]?.animate || ""}`
|
||||
})}
|
||||
{order.status.charAt(0).toUpperCase() + order.status.slice(1)}
|
||||
</div>
|
||||
{isOrderUnderpaid(order) && (
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 rounded text-[10px] bg-destructive/10 text-destructive border border-destructive/20 font-medium">
|
||||
{underpaidInfo?.percentage}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell text-sm text-muted-foreground">
|
||||
{new Date(order.orderDate).toLocaleDateString("en-GB", {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
<span className="ml-1 opacity-50 text-[10px]">
|
||||
{new Date(order.orderDate).toLocaleTimeString("en-GB", { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-cell text-sm text-muted-foreground">
|
||||
{order.paidAt ? new Date(order.paidAt).toLocaleDateString("en-GB", {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}) : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="hidden lg:table-cell">
|
||||
{order.telegramUsername ? (
|
||||
<span className="text-sm font-medium text-primary">@{order.telegramUsername}</span>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground italic">Guest</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground" asChild>
|
||||
<Link href={`/dashboard/orders/${order._id}`}>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
{(order.telegramBuyerId || order.telegramUsername) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-primary"
|
||||
asChild
|
||||
title={`Chat with customer${order.telegramUsername ? ` @${order.telegramUsername}` : ''}`}
|
||||
>
|
||||
<Link href={`/dashboard/chats/new?buyerId=${order.telegramBuyerId || order.telegramUsername}`}>
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</motion.tr>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between px-4 py-4 border-t border-zinc-800 bg-black/40">
|
||||
<div className="flex items-center justify-between px-4 py-4 border-t border-border/50 bg-background/50">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Page {currentPage} of {totalPages} ({totalOrders} total)
|
||||
</div>
|
||||
@@ -628,8 +637,9 @@ export default function OrderTable() {
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1 || loading}
|
||||
className="h-8"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<ChevronLeft className="h-3 w-3 mr-1" />
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
@@ -637,14 +647,15 @@ export default function OrderTable() {
|
||||
size="sm"
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages || loading}
|
||||
className="h-8"
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<ChevronRight className="h-3 w-3 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user