ecom / client /src /pages /orders.tsx
shashwatIDR's picture
Upload 106 files
1684141 verified
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>
);
}