Rocky_Mountain_Vending/components/order-management.tsx

551 lines
18 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Alert, AlertDescription } from "@/components/ui/alert"
import {
Search,
Eye,
Package,
DollarSign,
Calendar,
Mail,
Truck,
CheckCircle,
XCircle,
Clock,
AlertCircle,
} from "lucide-react"
interface Order {
id: string
customerId: string | null
customerEmail: string
items: OrderItem[]
totalAmount: number
currency: string
status: "pending" | "paid" | "fulfilled" | "cancelled" | "refunded"
paymentIntentId: string | null
stripeSessionId: string | null
createdAt: string
updatedAt: string
shippingAddress?: {
name: string
address: string
city: string
state: string
zipCode: string
country: string
}
}
interface OrderItem {
productId: string
productName: string
price: number
quantity: number
priceId: string
}
export function OrderManagement() {
const [orders, setOrders] = useState<Order[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("all")
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null)
const [showOrderDetail, setShowOrderDetail] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
// Fetch orders
const fetchOrders = async () => {
try {
setLoading(true)
setError(null)
const params = new URLSearchParams()
if (statusFilter !== "all") params.append("status", statusFilter)
const response = await fetch(`/api/orders?${params}`)
if (!response.ok) throw new Error("Failed to fetch orders")
const data = await response.json()
setOrders(data.orders)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch orders")
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchOrders()
}, [statusFilter])
// Handle order status update (placeholder)
const handleUpdateOrderStatus = async (
orderId: string,
newStatus: Order["status"]
) => {
try {
setLoading(true)
// In a real implementation, you would call an API to update the order
console.log(`Updating order ${orderId} to status: ${newStatus}`)
setOrders((prev) =>
prev.map((order) =>
order.id === orderId
? {
...order,
status: newStatus,
updatedAt: new Date().toISOString(),
}
: order
)
)
setSuccess(`Order status updated successfully`)
setShowOrderDetail(false)
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to update order status"
)
} finally {
setLoading(false)
}
}
// View order details
const handleViewOrder = (order: Order) => {
setSelectedOrder(order)
setShowOrderDetail(true)
}
// Filter orders based on search and status
const filteredOrders = orders.filter((order) => {
const matchesSearch =
order.customerEmail.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.id.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus =
statusFilter === "all" || order.status === statusFilter
return matchesSearch && matchesStatus
})
// Format date
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}
// Get status badge variant
const getStatusVariant = (status: Order["status"]) => {
switch (status) {
case "pending":
return "secondary"
case "paid":
return "default"
case "fulfilled":
return "default"
case "cancelled":
return "destructive"
case "refunded":
return "destructive"
default:
return "secondary"
}
}
// Get status icon
const getStatusIcon = (status: Order["status"]) => {
switch (status) {
case "pending":
return <Clock className="h-4 w-4" />
case "paid":
return <CheckCircle className="h-4 w-4" />
case "fulfilled":
return <Truck className="h-4 w-4" />
case "cancelled":
return <XCircle className="h-4 w-4" />
case "refunded":
return <AlertCircle className="h-4 w-4" />
default:
return null
}
}
// Clear alerts
useEffect(() => {
if (error || success) {
const timer = setTimeout(() => {
setError(null)
setSuccess(null)
}, 5000)
return () => clearTimeout(timer)
}
}, [error, success])
return (
<div className="space-y-6">
{/* Alerts */}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
{/* Header */}
<div>
<h2 className="text-2xl font-bold">Order Management</h2>
<p className="text-muted-foreground">View and manage customer orders</p>
</div>
{/* Filters */}
<Card>
<CardContent className="pt-6">
<div className="flex gap-4 items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search orders by email or ID..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-48">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="paid">Paid</SelectItem>
<SelectItem value="fulfilled">Fulfilled</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="refunded">Refunded</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Orders Table */}
<Card>
<CardHeader>
<CardTitle>Orders</CardTitle>
<CardDescription>Manage and track customer orders</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Order ID</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Items</TableHead>
<TableHead>Total</TableHead>
<TableHead>Status</TableHead>
<TableHead>Date</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredOrders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-mono text-sm">
{order.id}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<div className="max-w-[200px] truncate">
{order.customerEmail}
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground" />
<span>
{order.items.length} item
{order.items.length !== 1 ? "s" : ""}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4 text-muted-foreground" />
{order.totalAmount.toFixed(2)}
</div>
</TableCell>
<TableCell>
<Badge
variant={getStatusVariant(order.status)}
className="flex items-center gap-1"
>
{getStatusIcon(order.status)}
{order.status.charAt(0).toUpperCase() +
order.status.slice(1)}
</Badge>
</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3 text-muted-foreground" />
{formatDate(order.createdAt)}
</div>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => handleViewOrder(order)}
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{filteredOrders.length === 0 && !loading && (
<div className="text-center py-8 text-muted-foreground">
No orders found. {searchTerm && "Try adjusting your search."}
</div>
)}
</CardContent>
</Card>
{/* Order Detail Dialog */}
<Dialog open={showOrderDetail} onOpenChange={setShowOrderDetail}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
{selectedOrder && (
<>
<DialogHeader>
<DialogTitle>Order Details - {selectedOrder.id}</DialogTitle>
<DialogDescription>
{formatDate(selectedOrder.createdAt)}
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Customer Information */}
<Card>
<CardHeader>
<CardTitle className="text-lg">
Customer Information
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2">Contact</h4>
<div className="space-y-1 text-sm">
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-muted-foreground" />
<span>{selectedOrder.customerEmail}</span>
</div>
</div>
</div>
{selectedOrder.shippingAddress && (
<div>
<h4 className="font-medium mb-2">Shipping Address</h4>
<div className="space-y-1 text-sm">
<div>{selectedOrder.shippingAddress.name}</div>
<div>{selectedOrder.shippingAddress.address}</div>
<div>
{selectedOrder.shippingAddress.city},{" "}
{selectedOrder.shippingAddress.state}{" "}
{selectedOrder.shippingAddress.zipCode}
</div>
<div>{selectedOrder.shippingAddress.country}</div>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Order Items */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Order Items</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Price</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{selectedOrder.items.map((item, index) => (
<TableRow key={index}>
<TableCell className="font-medium">
{item.productName}
</TableCell>
<TableCell>${item.price.toFixed(2)}</TableCell>
<TableCell>{item.quantity}</TableCell>
<TableCell>
${(item.price * item.quantity).toFixed(2)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="mt-4 pt-4 border-t">
<div className="flex justify-between items-center">
<span className="font-medium">Total:</span>
<div className="flex items-center gap-1 text-lg">
<DollarSign className="h-5 w-5 text-muted-foreground" />
{selectedOrder.totalAmount.toFixed(2)}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Order Status */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Order Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge
variant={getStatusVariant(selectedOrder.status)}
className="flex items-center gap-1"
>
{getStatusIcon(selectedOrder.status)}
{selectedOrder.status.charAt(0).toUpperCase() +
selectedOrder.status.slice(1)}
</Badge>
<span className="text-sm text-muted-foreground">
Last updated: {formatDate(selectedOrder.updatedAt)}
</span>
</div>
<div className="flex gap-2">
{selectedOrder.status === "paid" && (
<>
<Button
variant="outline"
size="sm"
onClick={() =>
handleUpdateOrderStatus(
selectedOrder.id,
"fulfilled"
)
}
>
Mark as Fulfilled
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
handleUpdateOrderStatus(
selectedOrder.id,
"cancelled"
)
}
>
Cancel Order
</Button>
</>
)}
{selectedOrder.status === "fulfilled" && (
<Button
variant="outline"
size="sm"
onClick={() =>
handleUpdateOrderStatus(
selectedOrder.id,
"refunded"
)
}
>
Process Refund
</Button>
)}
</div>
</div>
</CardContent>
</Card>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowOrderDetail(false)}
>
Close
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
</div>
)
}