Spaces:
Running
Running
import { useLocation } from "wouter"; | |
import { useAuth } from "@/hooks/use-auth"; | |
import { useQuery, useMutation } from "@tanstack/react-query"; | |
import { ordersApi } from "@/lib/api"; | |
import { apiRequest, queryClient } from "@/lib/queryClient"; | |
import Header from "@/components/layout/header"; | |
import { useToast } from "@/hooks/use-toast"; | |
import { Package, ArrowLeft, Eye, RotateCcw, X } from "lucide-react"; | |
import { useState } from "react"; | |
export default function Orders() { | |
const [, setLocation] = useLocation(); | |
const { user, isLoading } = useAuth(); | |
const [searchQuery, setSearchQuery] = useState(""); | |
const [activeCategory, setActiveCategory] = useState<'all' | 'delivered' | 'undelivered'>('all'); | |
const { toast } = useToast(); | |
// Always call useQuery hook to maintain consistent hook order | |
const { data: orders = [], isLoading: ordersLoading } = useQuery({ | |
queryKey: ['/api/orders'], | |
queryFn: async () => { | |
const response = await ordersApi.get(); | |
return response.json(); | |
}, | |
enabled: !!user, // Only fetch when user is authenticated | |
}); | |
// Always call useMutation hook to maintain consistent hook order | |
const cancelOrderMutation = useMutation({ | |
mutationFn: async (orderId: string) => { | |
const response = await apiRequest('PATCH', `/api/orders/${orderId}/cancel`); | |
return response.json(); | |
}, | |
onSuccess: () => { | |
queryClient.invalidateQueries({ queryKey: ['/api/orders'] }); | |
toast({ | |
title: "Order Cancelled", | |
description: "Your order has been successfully cancelled.", | |
}); | |
}, | |
onError: (error: any) => { | |
toast({ | |
title: "Failed to Cancel Order", | |
description: error.message || "Something went wrong. Please try again.", | |
variant: "destructive", | |
}); | |
}, | |
}); | |
// Show loading while authentication is being verified | |
if (isLoading) { | |
return ( | |
<div className="min-h-screen bg-gray-50 flex items-center justify-center"> | |
<div className="text-center"> | |
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div> | |
<p className="text-gray-600">Loading...</p> | |
</div> | |
</div> | |
); | |
} | |
// Redirect if not authenticated after loading completes | |
if (!user) { | |
setLocation('/auth'); | |
return null; | |
} | |
const getStatusColor = (status: string) => { | |
switch (status.toLowerCase()) { | |
case 'delivered': | |
return '#10b981'; // green | |
case 'shipped': | |
return '#3b82f6'; // blue | |
case 'pending': | |
return '#f59e0b'; // amber | |
case 'cancelled': | |
return '#ef4444'; // red | |
default: | |
return '#6b7280'; // gray | |
} | |
}; | |
const getStatusText = (status: string) => { | |
return status.charAt(0).toUpperCase() + status.slice(1); | |
}; | |
const isDelivered = (status: string) => { | |
return status.toLowerCase() === 'delivered'; | |
}; | |
const filteredOrders = orders.filter((order: any) => { | |
if (activeCategory === 'delivered') { | |
return isDelivered(order.status); | |
} else if (activeCategory === 'undelivered') { | |
return !isDelivered(order.status); | |
} | |
return true; // 'all' | |
}); | |
const handleTrackOrder = (orderId: string) => { | |
// For now, just show an alert - in a real app you'd open a tracking modal or page | |
alert(`Tracking order ${orderId.slice(0, 8)}...`); | |
}; | |
const handleReorder = (order: any) => { | |
// For now, just navigate to home - in a real app you'd add items to cart | |
alert('Items added to cart!'); | |
setLocation('/'); | |
}; | |
const handleCancelOrder = (orderId: string) => { | |
if (confirm('Are you sure you want to cancel this order? This action cannot be undone.')) { | |
cancelOrderMutation.mutate(orderId); | |
} | |
}; | |
return ( | |
<div className="min-h-screen bg-gray-50"> | |
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} /> | |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-testid="orders-page"> | |
{/* Back Button */} | |
<div className="mb-6"> | |
<button | |
onClick={() => setLocation('/profile')} | |
className="flex items-center text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md transition-colors" | |
data-testid="back-to-profile" | |
> | |
<ArrowLeft className="h-4 w-4 mr-2" /> | |
Back to Profile | |
</button> | |
</div> | |
{/* Header */} | |
<div className="mb-8"> | |
<h1 className="text-3xl font-bold text-gray-900" data-testid="orders-title"> | |
My Orders | |
</h1> | |
<p className="text-gray-600 mt-2">Track and manage your order history</p> | |
</div> | |
{/* Category Filters */} | |
<div className="mb-6"> | |
<div className="flex flex-wrap gap-2"> | |
<button | |
onClick={() => setActiveCategory('all')} | |
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${ | |
activeCategory === 'all' | |
? 'bg-primary text-primary-foreground' | |
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300' | |
}`} | |
data-testid="category-all" | |
> | |
All Orders ({orders.length}) | |
</button> | |
<button | |
onClick={() => setActiveCategory('delivered')} | |
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${ | |
activeCategory === 'delivered' | |
? 'bg-green-600 text-white' | |
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300' | |
}`} | |
data-testid="category-delivered" | |
> | |
Delivered ({orders.filter((order: any) => isDelivered(order.status)).length}) | |
</button> | |
<button | |
onClick={() => setActiveCategory('undelivered')} | |
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${ | |
activeCategory === 'undelivered' | |
? 'bg-orange-600 text-white' | |
: 'bg-white text-gray-700 hover:bg-gray-100 border border-gray-300' | |
}`} | |
data-testid="category-undelivered" | |
> | |
Undelivered ({orders.filter((order: any) => !isDelivered(order.status)).length}) | |
</button> | |
</div> | |
</div> | |
{/* Orders List */} | |
<section className="bg-white rounded-lg shadow-sm border border-gray-200"> | |
<header className="px-6 py-4 border-b border-gray-200"> | |
<h2 className="text-lg font-semibold text-gray-900 flex items-center"> | |
<Package className="h-5 w-5 mr-2" /> | |
Order History | |
</h2> | |
</header> | |
<div className="p-6"> | |
{ordersLoading ? ( | |
<div className="space-y-4"> | |
{[...Array(3)].map((_, i) => ( | |
<div key={i} className="border rounded-lg p-4"> | |
<div className="h-6 bg-gray-200 rounded w-48 mb-2 animate-pulse"></div> | |
<div className="h-4 bg-gray-200 rounded w-32 mb-4 animate-pulse"></div> | |
<div className="space-y-2"> | |
<div className="h-12 bg-gray-200 rounded w-full animate-pulse"></div> | |
<div className="h-12 bg-gray-200 rounded w-full animate-pulse"></div> | |
</div> | |
</div> | |
))} | |
</div> | |
) : filteredOrders.length > 0 ? ( | |
<div className="space-y-6" data-testid="orders-list"> | |
{filteredOrders.map((order: any) => ( | |
<article key={order.id} className="border border-gray-200 rounded-lg p-4 sm:p-6 hover:shadow-md transition-shadow" data-testid={`order-${order.id}`}> | |
{/* Order Header */} | |
<header className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4"> | |
<div className="flex-1 min-w-0"> | |
<h3 className="text-lg font-semibold text-gray-900 truncate" data-testid={`order-id-${order.id}`}> | |
Order #{order.id.slice(0, 8).toUpperCase()} | |
</h3> | |
<p className="text-sm text-gray-500" data-testid={`order-date-${order.id}`}> | |
Placed on {new Date(order.createdAt).toLocaleDateString('en-US', { | |
year: 'numeric', | |
month: 'long', | |
day: 'numeric' | |
})} | |
</p> | |
</div> | |
<span | |
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium text-white w-fit" | |
style={{ backgroundColor: getStatusColor(order.status) }} | |
data-testid={`order-status-${order.id}`} | |
> | |
{getStatusText(order.status)} | |
</span> | |
</header> | |
{/* Order Items */} | |
{order.items && order.items.length > 0 && ( | |
<div className="space-y-3 mb-4"> | |
{order.items.map((item: any) => ( | |
<div key={item.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg" data-testid={`order-item-${item.id}`}> | |
<div className="w-12 h-12 sm:w-16 sm:h-16 bg-white rounded-md overflow-hidden border flex-shrink-0"> | |
{item.product?.images && item.product.images.length > 0 ? ( | |
<img | |
src={item.product.images[0]} | |
alt={item.product.title} | |
className="w-full h-full object-cover" | |
/> | |
) : ( | |
<div className="w-full h-full bg-gray-200 flex items-center justify-center"> | |
<Package className="h-4 w-4 sm:h-6 sm:w-6 text-gray-400" /> | |
</div> | |
)} | |
</div> | |
<div className="flex-1 min-w-0"> | |
<h4 className="font-medium text-gray-900 text-sm sm:text-base truncate" data-testid={`order-item-title-${item.id}`}> | |
{item.product?.title || 'Unknown Product'} | |
</h4> | |
<p className="text-xs sm:text-sm text-gray-500 mb-1" data-testid={`order-item-seller-${item.id}`}> | |
Sold by {item.product?.store?.name || 'Unknown Seller'} | |
</p> | |
<div className="flex items-center justify-between"> | |
<p className="text-xs sm:text-sm text-gray-500" data-testid={`order-item-quantity-${item.id}`}> | |
Qty: {item.quantity} | |
</p> | |
<p className="font-semibold text-gray-900 text-sm sm:text-base" data-testid={`order-item-price-${item.id}`}> | |
₹{parseFloat(item.price).toFixed(2)} | |
</p> | |
</div> | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
{/* Order Summary */} | |
<footer className="pt-4 border-t border-gray-200 space-y-3"> | |
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3"> | |
<div> | |
<span className="text-lg font-bold text-gray-900 block" data-testid={`order-total-${order.id}`}> | |
Total: ₹{parseFloat(order.total).toFixed(2)} | |
</span> | |
<p className="text-sm text-gray-500"> | |
Payment: {order.paymentMethod === 'cod' ? 'Cash on Delivery' : 'UPI'} | |
</p> | |
</div> | |
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3"> | |
<button | |
onClick={() => handleTrackOrder(order.id)} | |
className="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors w-full sm:w-auto" | |
data-testid={`track-order-${order.id}`} | |
> | |
<Eye className="h-4 w-4 mr-2" /> | |
Track Order | |
</button> | |
{order.status === 'pending' && ( | |
<button | |
onClick={() => handleCancelOrder(order.id)} | |
disabled={cancelOrderMutation.isPending} | |
className="flex items-center justify-center px-4 py-2 text-sm font-medium text-red-600 bg-white border border-red-300 rounded-md hover:bg-red-50 hover:text-red-700 transition-colors w-full sm:w-auto disabled:opacity-50" | |
data-testid={`cancel-order-${order.id}`} | |
> | |
<X className="h-4 w-4 mr-2" /> | |
{cancelOrderMutation.isPending ? 'Cancelling...' : 'Cancel Order'} | |
</button> | |
)} | |
<button | |
onClick={() => handleReorder(order)} | |
className="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors w-full sm:w-auto" | |
data-testid={`reorder-${order.id}`} | |
> | |
<RotateCcw className="h-4 w-4 mr-2" /> | |
Reorder | |
</button> | |
</div> | |
</div> | |
</footer> | |
</article> | |
))} | |
</div> | |
) : ( | |
<div className="text-center py-12" data-testid="no-orders"> | |
<Package className="h-16 w-16 text-gray-400 mx-auto mb-4" /> | |
<h3 className="text-lg font-medium text-gray-900 mb-2"> | |
{activeCategory === 'all' ? 'No orders yet' : `No ${activeCategory} orders`} | |
</h3> | |
<p className="text-gray-500 mb-6"> | |
{activeCategory === 'all' | |
? "You haven't placed any orders yet. Start shopping to see your orders here!" | |
: `You don't have any ${activeCategory} orders.` | |
} | |
</p> | |
<button | |
onClick={() => setLocation('/')} | |
className="inline-flex items-center px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white font-medium rounded-md hover:from-blue-600 hover:to-purple-600 transition-all" | |
data-testid="start-shopping" | |
> | |
Start Shopping | |
</button> | |
</div> | |
)} | |
</div> | |
</section> | |
</div> | |
</div> | |
); | |
} |