UNPKG

ajinkya-mhetre-mern

Version:

A MERN starter with frontend and backend folders

476 lines (438 loc) 19.3 kB
import React, { useState, useEffect } from 'react' import { useAuth } from '../../contexts/AuthContext' const baseUrl = import.meta.env.VITE_API_URL; function UserOrder() { const { token } = useAuth() const [orders, setOrders] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [selectedOrder, setSelectedOrder] = useState(null) const [cancelling, setCancelling] = useState(null) const [currentPage, setCurrentPage] = useState(1) const [ordersPerPage] = useState(10) const [searchTerm, setSearchTerm] = useState('') const [statusFilter, setStatusFilter] = useState('all') // Fetch all orders const fetchOrders = async () => { try { setLoading(true) const response = await fetch(`${baseUrl}/customer/orders`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } // Removed body - GET requests cannot have a body }) if (!response.ok) { throw new Error('Failed to fetch orders') } const data = await response.json() if (data.success) { setOrders(data.orders) } else { throw new Error('Failed to fetch orders') } } catch (err) { setError(err.message) } finally { setLoading(false) } } // Fetch single order details const fetchOrderDetails = async (orderId) => { try { const response = await fetch(`${baseUrl}/customer/orders/${orderId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } // Removed body - GET requests cannot have a body }) if (!response.ok) { throw new Error('Failed to fetch order details') } const data = await response.json() if (data.success) { setSelectedOrder(data.order) } } catch (err) { setError(err.message) } } // Cancel order const cancelOrder = async (orderId) => { try { setCancelling(orderId) const response = await fetch(`${baseUrl}/customer/orders/${orderId}/cancel`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } // You can keep body for PUT requests, but since you're sending token in header, // the body with token is redundant. Remove it unless your API specifically requires it. // body: JSON.stringify({ token }) }) if (!response.ok) { throw new Error('Failed to cancel order') } // Refresh orders after cancellation await fetchOrders() if (selectedOrder && selectedOrder._id === orderId) { setSelectedOrder(null) } } catch (err) { setError(err.message) } finally { setCancelling(null) } } useEffect(() => { if (token) { fetchOrders() } }, [token]) // Filter and search orders const filteredOrders = orders.filter(order => { const matchesSearch = order.items.some(item => item.product.name.toLowerCase().includes(searchTerm.toLowerCase()) ) || order._id.toLowerCase().includes(searchTerm.toLowerCase()) const matchesStatus = statusFilter === 'all' || order.status === statusFilter return matchesSearch && matchesStatus }) // Pagination const indexOfLastOrder = currentPage * ordersPerPage const indexOfFirstOrder = indexOfLastOrder - ordersPerPage const currentOrders = filteredOrders.slice(indexOfFirstOrder, indexOfLastOrder) const totalPages = Math.ceil(filteredOrders.length / ordersPerPage) // Helper functions const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString('en-IN', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } const getStatusColor = (status) => { const colors = { 'placed': 'bg-blue-100 text-blue-800', 'confirmed': 'bg-green-100 text-green-800', 'shipped': 'bg-purple-100 text-purple-800', 'delivered': 'bg-green-100 text-green-800', 'cancelled': 'bg-red-100 text-red-800' } return colors[status] || 'bg-gray-100 text-gray-800' } const getPaymentStatusColor = (status) => { const colors = { 'pending': 'bg-yellow-100 text-yellow-800', 'paid': 'bg-green-100 text-green-800', 'failed': 'bg-red-100 text-red-800' } return colors[status] || 'bg-gray-100 text-gray-800' } if (loading) { return ( <div className="flex justify-center items-center min-h-screen"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600"></div> </div> ) } if (error) { return ( <div className="max-w-6xl mx-auto p-6"> <div className="bg-red-50 border border-red-200 rounded-lg p-4"> <h3 className="text-red-800 font-medium">Error</h3> <p className="text-red-600">{error}</p> <button onClick={fetchOrders} className="mt-2 bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700" > Retry </button> </div> </div> ) } return ( <div className="max-w-6xl mx-auto p-6"> <h1 className="text-3xl font-bold text-gray-800 mb-6">My Orders</h1> {/* Search and Filter */} <div className="mb-6 flex flex-col sm:flex-row gap-4"> <div className="flex-1"> <input type="text" placeholder="Search orders by product name or order ID..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" /> </div> <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" > <option value="all">All Status</option> <option value="placed">Placed</option> <option value="confirmed">Confirmed</option> <option value="shipped">Shipped</option> <option value="delivered">Delivered</option> <option value="cancelled">Cancelled</option> </select> </div> {/* Orders Count */} <div className="mb-4 text-gray-600"> Showing {currentOrders.length} of {filteredOrders.length} orders </div> {/* Orders List */} {currentOrders.length === 0 ? ( <div className="text-center py-12"> <div className="text-gray-400 text-6xl mb-4">📦</div> <h3 className="text-xl font-medium text-gray-600 mb-2">No orders found</h3> <p className="text-gray-500"> {orders.length === 0 ? "You haven't placed any orders yet." : "No orders match your search criteria."} </p> </div> ) : ( <div className="space-y-4"> {currentOrders.map((order) => ( <div key={order._id} className="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow"> <div className="p-6"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between mb-4"> <div> <h3 className="text-lg font-semibold text-gray-800 mb-1"> Order #{order._id.slice(-8)} </h3> <p className="text-gray-600 text-sm"> Placed on {formatDate(order.createdAt)} </p> </div> <div className="flex flex-col sm:flex-row gap-2 mt-3 lg:mt-0"> <span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(order.status)}`}> {order.status.charAt(0).toUpperCase() + order.status.slice(1)} </span> <span className={`px-3 py-1 rounded-full text-xs font-medium ${getPaymentStatusColor(order.paymentStatus)}`}> Payment: {order.paymentStatus.charAt(0).toUpperCase() + order.paymentStatus.slice(1)} </span> </div> </div> {/* Order Items */} <div className="mb-4"> <h4 className="font-medium text-gray-700 mb-2">Items ({order.items.length})</h4> <div className="space-y-2"> {order.items.map((item) => ( <div key={item._id} className="flex items-center gap-4 bg-gray-50 p-3 rounded-lg"> <img src={item.product.images[0]?.url} alt={item.product.name} className="w-12 h-12 object-cover rounded-lg" /> <div className="flex-1"> <p className="font-medium text-gray-800">{item.product.name}</p> <p className="text-sm text-gray-600"> Farmer: {item.farmer.name} • Qty: {item.quantity} • ₹{item.price} </p> </div> </div> ))} </div> </div> {/* Shipping Address */} <div className="mb-4"> <h4 className="font-medium text-gray-700 mb-1">Shipping Address</h4> <p className="text-sm text-gray-600"> {order.shippingAddress.street}, {order.shippingAddress.city}, {order.shippingAddress.state} - {order.shippingAddress.zipCode} </p> </div> {/* Order Total and Actions */} <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between pt-4 border-t border-gray-200"> <div className="mb-3 sm:mb-0"> <span className="text-lg font-bold text-gray-800">Total: ₹{order.totalAmount}</span> </div> <div className="flex gap-2"> <button onClick={() => fetchOrderDetails(order._id)} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" > View Details </button> {order.status === 'placed' && ( <button onClick={() => cancelOrder(order._id)} disabled={cancelling === order._id} className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > {cancelling === order._id ? 'Cancelling...' : 'Cancel Order'} </button> )} </div> </div> </div> </div> ))} </div> )} {/* Pagination */} {totalPages > 1 && ( <div className="flex justify-center items-center mt-8 gap-2"> <button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" > Previous </button> <div className="flex gap-1"> {[...Array(totalPages)].map((_, index) => { const pageNum = index + 1 if (pageNum === 1 || pageNum === totalPages || (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)) { return ( <button key={pageNum} onClick={() => setCurrentPage(pageNum)} className={`px-3 py-2 rounded-lg ${ currentPage === pageNum ? 'bg-green-600 text-white' : 'border border-gray-300 hover:bg-gray-50' }`} > {pageNum} </button> ) } else if (pageNum === currentPage - 2 || pageNum === currentPage + 2) { return <span key={pageNum} className="px-2 py-2">...</span> } return null })} </div> <button onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))} disabled={currentPage === totalPages} className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" > Next </button> </div> )} {/* Order Details Modal */} {selectedOrder && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto"> <div className="p-6"> <div className="flex items-center justify-between mb-6"> <h2 className="text-2xl font-bold text-gray-800"> Order Details #{selectedOrder._id.slice(-8)} </h2> <button onClick={() => setSelectedOrder(null)} className="text-gray-400 hover:text-gray-600 text-2xl" > × </button> </div> {/* Order Status Timeline */} <div className="mb-6"> <h3 className="font-semibold text-gray-700 mb-3">Order Status History</h3> <div className="space-y-2"> {selectedOrder.statusHistory.map((status, index) => ( <div key={status._id} className="flex items-center gap-3"> <div className={`w-3 h-3 rounded-full ${index === 0 ? 'bg-green-500' : 'bg-gray-300'}`}></div> <div> <p className="font-medium text-gray-800"> {status.status.charAt(0).toUpperCase() + status.status.slice(1)} </p> <p className="text-sm text-gray-600">{formatDate(status.timestamp)}</p> {status.note && <p className="text-sm text-gray-500">{status.note}</p>} </div> </div> ))} </div> </div> {/* Detailed Items */} <div className="mb-6"> <h3 className="font-semibold text-gray-700 mb-3">Order Items</h3> <div className="space-y-3"> {selectedOrder.items.map((item) => ( <div key={item._id} className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg"> <img src={item.product.images[0]?.url} alt={item.product.name} className="w-16 h-16 object-cover rounded-lg" /> <div className="flex-1"> <h4 className="font-medium text-gray-800">{item.product.name}</h4> <p className="text-sm text-gray-600 mb-1"> Farmer: {item.farmer.name} </p> {item.farmer.phone && ( <p className="text-sm text-gray-600"> Contact: {item.farmer.phone} </p> )} </div> <div className="text-right"> <p className="font-medium">Qty: {item.quantity}</p> <p className="text-lg font-bold text-green-600">₹{item.price}</p> </div> </div> ))} </div> </div> {/* Shipping & Payment Info */} <div className="grid md:grid-cols-2 gap-6 mb-6"> <div> <h3 className="font-semibold text-gray-700 mb-2">Shipping Address</h3> <div className="bg-gray-50 p-3 rounded-lg"> <p className="text-sm text-gray-800"> {selectedOrder.shippingAddress.street}<br /> {selectedOrder.shippingAddress.city}, {selectedOrder.shippingAddress.state}<br /> {selectedOrder.shippingAddress.zipCode} </p> </div> </div> <div> <h3 className="font-semibold text-gray-700 mb-2">Payment Information</h3> <div className="bg-gray-50 p-3 rounded-lg"> <p className="text-sm text-gray-800 mb-1"> Status: <span className={`px-2 py-1 rounded text-xs ${getPaymentStatusColor(selectedOrder.paymentStatus)}`}> {selectedOrder.paymentStatus.charAt(0).toUpperCase() + selectedOrder.paymentStatus.slice(1)} </span> </p> <p className="text-lg font-bold text-green-600">Total: ₹{selectedOrder.totalAmount}</p> </div> </div> </div> {/* Action Buttons */} <div className="flex gap-3 pt-4 border-t"> {selectedOrder.status === 'placed' && ( <button onClick={() => { cancelOrder(selectedOrder._id) setSelectedOrder(null) }} disabled={cancelling === selectedOrder._id} className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50" > {cancelling === selectedOrder._id ? 'Cancelling...' : 'Cancel Order'} </button> )} <button onClick={() => setSelectedOrder(null)} className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors" > Close </button> </div> </div> </div> </div> )} </div> ) } export default UserOrder