Rocky_Mountain_Vending/components/order-management.tsx
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

481 lines
17 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>
)
}