Files
padmaja/app/admin/orders/[id]/page.tsx
2026-01-17 14:17:42 +05:30

460 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import {
ArrowLeft,
Package,
User,
CreditCard,
Truck,
CheckCircle,
XCircle,
Clock,
MapPin,
Phone,
Mail
} from 'lucide-react'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { toast } from 'sonner'
import { motion } from 'framer-motion'
import Link from 'next/link'
import Image from 'next/image'
interface OrderItem {
id: string
quantity: number
price: number
product: {
id: string
name: string
images: string[]
sku: string
price: number
}
}
interface ShippingAddress {
firstName: string
lastName: string
company?: string | null
address1: string
address2?: string | null
city: string
state: string
zipCode: string
country: string
phone?: string | null
}
interface Order {
id: string
total: number
status: 'PENDING' | 'PAID' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'
createdAt: string
updatedAt: string
razorpayOrderId: string | null
razorpayPaymentId: string | null
shippingAddress: ShippingAddress | null
user: {
id: string
name: string
email: string
phone: string | null
}
orderItems: OrderItem[]
}
const statusColors = {
PENDING: 'bg-yellow-100 text-yellow-800 border-yellow-200',
PAID: 'bg-blue-100 text-blue-800 border-blue-200',
SHIPPED: 'bg-purple-100 text-purple-800 border-purple-200',
DELIVERED: 'bg-green-100 text-green-800 border-green-200',
CANCELLED: 'bg-red-100 text-red-800 border-red-200'
}
const statusIcons = {
PENDING: Clock,
PAID: CreditCard,
SHIPPED: Truck,
DELIVERED: CheckCircle,
CANCELLED: XCircle
}
export default function AdminOrderDetail() {
const params = useParams()
const router = useRouter()
const [order, setOrder] = useState<Order | null>(null)
const [loading, setLoading] = useState(true)
const [updating, setUpdating] = useState(false)
const orderId = params.id as string
const fetchOrder = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(`/api/admin/orders/${orderId}`)
if (!response.ok) {
throw new Error('Failed to fetch order')
}
const data = await response.json()
setOrder(data)
} catch (error) {
console.error('Error fetching order:', error)
toast.error('Failed to load order details')
} finally {
setLoading(false)
}
}, [orderId])
const updateOrderStatus = async (newStatus: string) => {
try {
setUpdating(true)
const response = await fetch(`/api/admin/orders/${orderId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ status: newStatus }),
})
if (!response.ok) {
throw new Error('Failed to update order status')
}
const data = await response.json()
setOrder(data)
toast.success('Order status updated successfully')
} catch (error) {
console.error('Error updating order:', error)
toast.error('Failed to update order status')
} finally {
setUpdating(false)
}
}
useEffect(() => {
if (orderId) {
fetchOrder()
}
}, [orderId, fetchOrder])
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
<div className="h-96 bg-gray-200 rounded"></div>
</div>
<div className="space-y-4">
<div className="h-48 bg-gray-200 rounded"></div>
<div className="h-32 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
)
}
if (!order) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Package className="h-12 w-12 text-gray-400 mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Order Not Found</h3>
<p className="text-gray-600 mb-4">The order you're looking for doesn't exist.</p>
<Link href="/admin/orders">
<Button>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Button>
</Link>
</CardContent>
</Card>
</div>
)
}
const StatusIcon = statusIcons[order.status] || Clock
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="container mx-auto px-4 py-8"
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Link href="/admin/orders">
<Button variant="outline" size="sm">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Orders
</Button>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Order #{order.id}</h1>
<p className="text-gray-600">
Placed on {new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</p>
</div>
</div>
<Badge className={statusColors[order.status]}>
<StatusIcon className="h-4 w-4 mr-1" />
{order.status}
</Badge>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Order Items */}
<div className="lg:col-span-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Package className="h-5 w-5 mr-2" />
Order Items ({order.orderItems?.length || 0})
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{order.orderItems && order.orderItems.length > 0 ? (
order.orderItems.map((item) => (
<div key={item.id} className="flex items-center space-x-4 p-4 border rounded-lg bg-gray-50">
<div className="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center">
{item.product?.images && item.product.images.length > 0 ? (
<Image
src={item.product.images[0]}
alt={item.product.name || 'Product'}
width={64}
height={64}
className="w-full h-full object-cover rounded-lg"
/>
) : (
<Package className="h-8 w-8 text-gray-400" />
)}
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">{item.product?.name || 'Unknown Product'}</h4>
<p className="text-sm text-gray-600">SKU: {item.product?.sku || 'N/A'}</p>
<div className="flex items-center space-x-4 mt-1">
<span className="text-sm text-gray-600">Qty: {item.quantity}</span>
<span className="text-sm text-gray-600">Price: {item.price.toFixed(2)} each</span>
</div>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">
{(item.price * item.quantity).toFixed(2)}
</p>
<p className="text-xs text-gray-500">
{item.quantity} × {item.price.toFixed(2)}
</p>
</div>
</div>
))
) : (
<div className="text-center py-8">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No items found in this order</p>
</div>
)}
</div>
<Separator className="my-4" />
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">Total Amount:</span>
<span className="text-xl font-bold text-green-600">{order.total?.toFixed(2) || '0.00'}</span>
</div>
</CardContent>
</Card>
</div>
{/* Order Details Sidebar */}
<div className="space-y-6">
{/* Order Actions */}
<Card>
<CardHeader>
<CardTitle>Order Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<label className="text-sm font-medium text-gray-700 mb-2 block">
Update Order Status
</label>
<Select
value={order.status}
onValueChange={(value) => updateOrderStatus(value)}
disabled={updating}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="PENDING">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-yellow-600" />
Pending
</div>
</SelectItem>
<SelectItem value="PAID">
<div className="flex items-center">
<CreditCard className="h-4 w-4 mr-2 text-blue-600" />
Paid
</div>
</SelectItem>
<SelectItem value="SHIPPED">
<div className="flex items-center">
<Truck className="h-4 w-4 mr-2 text-purple-600" />
Shipped
</div>
</SelectItem>
<SelectItem value="DELIVERED">
<div className="flex items-center">
<CheckCircle className="h-4 w-4 mr-2 text-green-600" />
Delivered
</div>
</SelectItem>
<SelectItem value="CANCELLED">
<div className="flex items-center">
<XCircle className="h-4 w-4 mr-2 text-red-600" />
Cancelled
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{updating && (
<div className="text-sm text-gray-500 flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
Updating order status...
</div>
)}
</CardContent>
</Card>
{/* Customer Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<User className="h-5 w-5 mr-2" />
Customer Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center space-x-3">
<User className="h-4 w-4 text-gray-500" />
<span>{order.user?.name || 'Unknown User'}</span>
</div>
<div className="flex items-center space-x-3">
<Mail className="h-4 w-4 text-gray-500" />
<span className="text-sm">{order.user?.email || 'No email'}</span>
</div>
{order.user?.phone && (
<div className="flex items-center space-x-3">
<Phone className="h-4 w-4 text-gray-500" />
<span>{order.user.phone}</span>
</div>
)}
</CardContent>
</Card>
{/* Shipping Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<MapPin className="h-5 w-5 mr-2" />
Shipping Address
</CardTitle>
</CardHeader>
<CardContent>
{order.shippingAddress ? (
<div className="space-y-2">
<p className="font-semibold">
{order.shippingAddress.firstName} {order.shippingAddress.lastName}
</p>
{order.shippingAddress.company && (
<p className="text-sm text-gray-600">{order.shippingAddress.company}</p>
)}
<p className="text-sm">{order.shippingAddress.address1}</p>
{order.shippingAddress.address2 && (
<p className="text-sm">{order.shippingAddress.address2}</p>
)}
<p className="text-sm">
{order.shippingAddress.city}, {order.shippingAddress.state} {order.shippingAddress.zipCode}
</p>
<p className="text-sm">{order.shippingAddress.country}</p>
{order.shippingAddress.phone && (
<p className="text-sm flex items-center mt-2">
<Phone className="h-4 w-4 mr-1" />
{order.shippingAddress.phone}
</p>
)}
</div>
) : (
<p className="text-sm text-gray-500">No shipping address provided</p>
)}
</CardContent>
</Card>
{/* Payment Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<CreditCard className="h-5 w-5 mr-2" />
Payment Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<span className="text-sm font-medium text-gray-500">Payment Status:</span>
<Badge className={`ml-2 ${statusColors[order.status]}`}>
{order.status}
</Badge>
</div>
{order.razorpayOrderId && (
<div>
<span className="text-sm font-medium text-gray-500">Razorpay Order ID:</span>
<p className="text-sm font-mono bg-gray-50 p-2 rounded mt-1">{order.razorpayOrderId}</p>
</div>
)}
{order.razorpayPaymentId && (
<div>
<span className="text-sm font-medium text-gray-500">Payment ID:</span>
<p className="text-sm font-mono bg-gray-50 p-2 rounded mt-1">{order.razorpayPaymentId}</p>
</div>
)}
<Separator />
<div>
<span className="text-sm font-medium text-gray-500">Total Amount:</span>
<p className="text-lg font-bold text-green-600">{order.total?.toFixed(2) || '0.00'}</p>
</div>
</CardContent>
</Card>
</div>
</div>
</motion.div>
)
}