This commit is contained in:
NotII
2025-02-25 13:12:20 +00:00
parent 56c1b4d7b9
commit f498624eff
2 changed files with 357 additions and 237 deletions

View File

@@ -24,7 +24,7 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Clipboard, Truck, Package, ArrowRight } from "lucide-react"; import { Clipboard, Truck, Package, ArrowRight, ChevronDown } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -38,6 +38,12 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import Layout from "@/components/layout/layout";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
interface Order { interface Order {
orderId: string; orderId: string;
@@ -58,16 +64,20 @@ interface Order {
const getStatusVariant = (status: string) => { const getStatusVariant = (status: string) => {
switch (status) { switch (status) {
case 'acknowledged':
return 'secondary';
case 'paid': case 'paid':
return 'paid'; return 'default';
case 'shipped': case 'shipped':
return 'shipped'; return 'default';
case 'completed': case 'completed':
return 'completed'; return 'default';
case 'cancelled': case 'cancelled':
return 'destructive'; return 'destructive';
case 'unpaid':
return 'secondary';
default: default:
return 'unpaid'; return 'secondary';
} }
}; };
@@ -87,6 +97,7 @@ export default function OrderDetailsPage() {
const [currentOrderNumber, setCurrentOrderNumber] = useState(0); const [currentOrderNumber, setCurrentOrderNumber] = useState(0);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [isAcknowledging, setIsAcknowledging] = useState(false);
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
@@ -177,9 +188,9 @@ export default function OrderDetailsPage() {
} }
}; };
const handleDiscardOrder = async () => { const handleMarkAsAcknowledged = async () => {
try { try {
setIsDiscarding(true); setIsAcknowledging(true);
const authToken = document.cookie.split("Authorization=")[1]; const authToken = document.cookie.split("Authorization=")[1];
const response = await fetchData( const response = await fetchData(
`${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}`, `${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}`,
@@ -189,19 +200,47 @@ export default function OrderDetailsPage() {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${authToken}`, Authorization: `Bearer ${authToken}`,
}, },
body: JSON.stringify({ status: "cancelled" }), body: JSON.stringify({ status: "acknowledged" }),
} }
); );
if (response && response.message === "Order status updated successfully") { if (response && response.message === "Order status updated successfully") {
setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "cancelled" } : null); setOrder((prevOrder) => prevOrder ? { ...prevOrder, status: "acknowledged" } : null);
toast.success("Order discarded successfully!"); toast.success("Order marked as acknowledged!");
} else { } else {
throw new Error(response.error || "Failed to discard order"); throw new Error(response.error || "Failed to mark order as acknowledged");
} }
} catch (error: any) { } catch (error: any) {
console.error("Failed to discard order:", error); console.error("Failed to mark order as acknowledged:", error);
toast.error(error.message || "Failed to discard order"); toast.error(error.message || "Failed to mark order as acknowledged");
} finally {
setIsAcknowledging(false);
}
};
const handleDiscardOrder = async () => {
try {
setIsDiscarding(true);
const authToken = document.cookie.split("Authorization=")[1];
const response = await fetchData(
`${process.env.NEXT_PUBLIC_API_URL}/orders/${orderId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${authToken}`,
},
}
);
if (response && response.message === "Order deleted successfully") {
toast.success("Order deleted successfully!");
router.push('/dashboard/orders');
} else {
throw new Error(response.error || "Failed to delete order");
}
} catch (error: any) {
console.error("Failed to delete order:", error);
toast.error(error.message || "Failed to delete order");
} finally { } finally {
setIsDiscarding(false); setIsDiscarding(false);
} }
@@ -345,11 +384,20 @@ export default function OrderDetailsPage() {
}; };
if (loading) if (loading)
return <div className="text-center py-10">Loading order details...</div>; return (
<Layout>
<div className="text-center py-10">Loading order details...</div>
</Layout>
);
if (error) if (error)
return <div className="text-center text-red-500 py-10">Error: {error}</div>; return (
<Layout>
<div className="text-center text-red-500 py-10">Error: {error}</div>
</Layout>
);
return ( return (
<Layout>
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@@ -357,12 +405,64 @@ export default function OrderDetailsPage() {
<h1 className="text-3xl font-bold">Order Details: {order?.orderId}</h1> <h1 className="text-3xl font-bold">Order Details: {order?.orderId}</h1>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant={getStatusVariant(order?.status || '')}> <Badge
variant={getStatusVariant(order?.status || '')}
className={`${
order?.status === 'acknowledged'
? '!bg-purple-600 hover:!bg-purple-700 text-white'
: order?.status === 'paid'
? 'bg-emerald-600 hover:bg-emerald-700 text-white'
: order?.status === 'shipped'
? 'bg-blue-600 hover:bg-blue-700 text-white'
: order?.status === 'completed'
? 'bg-green-600 hover:bg-green-700 text-white'
: ''
}`}
>
{order?.status} {order?.status}
</Badge> </Badge>
</div> </div>
</div> </div>
{/* Order Navigation - Moved to top */}
<div className="flex justify-between items-center border-b pb-4">
<div className="w-[140px]">
{nextOrderId && (
<Button
variant="outline"
size="lg"
onClick={() => {
setLoading(true);
router.push(`/dashboard/orders/${nextOrderId}`);
}}
className="flex items-center"
>
<span className="mr-2"></span>
Older
</Button>
)}
</div>
<div className="text-center text-sm text-muted-foreground">
Navigate Orders
</div>
<div className="w-[140px] flex justify-end">
{prevOrderId && (
<Button
variant="outline"
size="lg"
onClick={() => {
setLoading(true);
router.push(`/dashboard/orders/${prevOrderId}`);
}}
className="flex items-center"
>
Newer
<span className="ml-2"></span>
</Button>
)}
</div>
</div>
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
<Card> <Card>
<CardHeader> <CardHeader>
@@ -466,9 +566,16 @@ export default function OrderDetailsPage() {
</Card> </Card>
{/* Add Crypto Transaction Details */} {/* Add Crypto Transaction Details */}
<Collapsible>
<div className="rounded-lg border bg-card text-card-foreground shadow-sm"> <div className="rounded-lg border bg-card text-card-foreground shadow-sm">
<div className="p-6"> <CollapsibleTrigger className="w-full">
<h3 className="text-lg font-semibold mb-4">Crypto Transactions</h3> <div className="p-6 flex items-center justify-between">
<h3 className="text-lg font-semibold">Crypto Transactions</h3>
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 [&[data-state=open]]:rotate-180" />
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="px-6 pb-6">
{order?.txid && order.txid.length > 0 ? ( {order?.txid && order.txid.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{order.txid.slice(order.txid.length > 2 ? 1 : 0).map((txid: string, index: number) => ( {order.txid.slice(order.txid.length > 2 ? 1 : 0).map((txid: string, index: number) => (
@@ -476,7 +583,7 @@ export default function OrderDetailsPage() {
key={index} key={index}
className="p-3 bg-muted rounded-md" className="p-3 bg-muted rounded-md"
> >
<code className="text-sm">{txid}</code> <code className="text-sm break-all">{txid}</code>
</div> </div>
))} ))}
</div> </div>
@@ -484,28 +591,30 @@ export default function OrderDetailsPage() {
<p className="text-muted-foreground">No crypto transactions associated with this order</p> <p className="text-muted-foreground">No crypto transactions associated with this order</p>
)} )}
</div> </div>
</CollapsibleContent>
</div> </div>
</Collapsible>
<div className="flex justify-between gap-4 mt-4"> <div className="flex justify-between gap-4">
<div> <div>
{order?.status !== "cancelled" && ( {order?.status !== "cancelled" && (
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button variant="destructive" size="lg" disabled={isDiscarding}> <Button variant="destructive" size="lg" disabled={isDiscarding}>
{isDiscarding ? "Discarding..." : "Discard Order"} {isDiscarding ? "Deleting..." : "Delete Order"}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle> <AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action will discard the order and cannot be undone. This action will permanently delete the order and cannot be undone.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDiscardOrder}> <AlertDialogAction onClick={handleDiscardOrder}>
Discard Order Delete Order
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@@ -513,7 +622,18 @@ export default function OrderDetailsPage() {
)} )}
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
{order?.status === "unpaid" && ( {(order?.status === "unpaid" || order?.status === "paid") && (
<Button
size="lg"
onClick={handleMarkAsAcknowledged}
disabled={isAcknowledging}
className="bg-purple-600 hover:bg-purple-700"
>
{isAcknowledging ? "Updating..." : "Mark as Acknowledged"}
</Button>
)}
{(order?.status === "unpaid" || order?.status === "acknowledged") && (
<Button <Button
size="lg" size="lg"
onClick={handleMarkAsPaid} onClick={handleMarkAsPaid}
@@ -537,43 +657,8 @@ export default function OrderDetailsPage() {
)} )}
</div> </div>
</div> </div>
{/* Order Navigation */}
<div className="flex justify-between items-center pt-8 mt-8 border-t">
<div className="w-[140px]">
{prevOrderId && (
<Button
variant="outline"
size="lg"
onClick={() => {
setLoading(true);
router.push(`/dashboard/orders/${prevOrderId}`);
}}
className="flex items-center"
>
<span className="mr-2"></span>
Newer
</Button>
)}
</div>
<div className="w-[140px] flex justify-end">
{nextOrderId && (
<Button
variant="outline"
size="lg"
onClick={() => {
setLoading(true);
router.push(`/dashboard/orders/${nextOrderId}`);
}}
className="flex items-center"
>
Older
<span className="ml-2"></span>
</Button>
)}
</div>
</div>
</div> </div>
</div> </div>
</Layout>
); );
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import React from "react";
import { import {
Table, Table,
TableBody, TableBody,
@@ -57,10 +58,11 @@ type SortableColumns = "orderId" | "totalPrice" | "status" | "orderDate";
interface StatusConfig { interface StatusConfig {
icon: React.ElementType; icon: React.ElementType;
color: string; color: string;
bgColor: string;
animate?: string; animate?: string;
} }
type OrderStatus = "paid" | "unpaid" | "confirming" | "shipped" | "completed" | "disputed" | "cancelled"; type OrderStatus = "paid" | "unpaid" | "acknowledged" | "shipped" | "completed" | "cancelled" | "confirming";
export default function OrderTable() { export default function OrderTable() {
const [orders, setOrders] = useState<Order[]>([]); const [orders, setOrders] = useState<Order[]>([]);
@@ -208,13 +210,42 @@ export default function OrderTable() {
}; };
const statusConfig: Record<OrderStatus, StatusConfig> = { const statusConfig: Record<OrderStatus, StatusConfig> = {
paid: { icon: CheckCircle2, color: "text-green-500" }, acknowledged: {
unpaid: { icon: XCircle, color: "text-red-500" }, icon: CheckCircle2,
confirming: { icon: Loader2, color: "text-yellow-500", animate: "animate-spin" }, color: "text-white",
shipped: { icon: Truck, color: "text-blue-500" }, bgColor: "bg-purple-600"
completed: { icon: CheckCircle2, color: "text-gray-500" }, },
disputed: { icon: XCircle, color: "text-orange-500" }, paid: {
cancelled: { icon: XCircle, color: "text-gray-400" } icon: CheckCircle2,
color: "text-white",
bgColor: "bg-emerald-600"
},
unpaid: {
icon: XCircle,
color: "text-white",
bgColor: "bg-red-500"
},
confirming: {
icon: Loader2,
color: "text-white",
bgColor: "bg-yellow-500",
animate: "animate-spin"
},
shipped: {
icon: Truck,
color: "text-white",
bgColor: "bg-blue-600"
},
completed: {
icon: CheckCircle2,
color: "text-white",
bgColor: "bg-green-600"
},
cancelled: {
icon: XCircle,
color: "text-white",
bgColor: "bg-gray-500"
}
}; };
return ( return (
@@ -317,8 +348,12 @@ export default function OrderTable() {
<TableCell>#{order.orderId}</TableCell> <TableCell>#{order.orderId}</TableCell>
<TableCell>£{order.totalPrice.toFixed(2)}</TableCell> <TableCell>£{order.totalPrice.toFixed(2)}</TableCell>
<TableCell> <TableCell>
<div className={`flex items-center ${statusConfig[order.status as OrderStatus]?.color || "text-red-500"}`}> <div className={`inline-flex items-center gap-2 px-3 py-1 rounded-full text-sm ${
<StatusIcon className={`h-4 w-4 mr-2 ${statusConfig[order.status as OrderStatus]?.animate || ""}`} /> 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)} {order.status.charAt(0).toUpperCase() + order.status.slice(1)}
</div> </div>
</TableCell> </TableCell>