UNPKG

ajinkya-mhetre-mern

Version:

A MERN starter with frontend and backend folders

754 lines (699 loc) 34.7 kB
import React, { useState, useEffect, useCallback, useMemo } from 'react' import { useAuth } from '../../contexts/AuthContext' import { Search, Filter, Package, MapPin, Calendar, DollarSign, Phone, Mail, Eye, Edit3, Truck, CheckCircle, XCircle, Clock, AlertCircle, MoreVertical, RefreshCw } from 'lucide-react' import Stats from '../../components/Framers/Stats' const baseUrl = import.meta.env.VITE_API_URL; function FramersMyOrders() { const { token } = useAuth() const [orders, setOrders] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [statusFilter, setStatusFilter] = useState('all') const [paymentFilter, setPaymentFilter] = useState('all') const [currentPage, setCurrentPage] = useState(1) const [selectedOrder, setSelectedOrder] = useState(null) const [showStatusModal, setShowStatusModal] = useState(false) const [showDetailsModal, setShowDetailsModal] = useState(false) const [updatingStatus, setUpdatingStatus] = useState(false) const [newStatus, setNewStatus] = useState('') const [statusNote, setStatusNote] = useState('') const [refreshing, setRefreshing] = useState(false) const ordersPerPage = 15 const statusOptions = ['placed', 'processing', 'dispatched', 'delivered', 'cancelled'] // Fetch orders from API const fetchOrders = useCallback(async (showRefreshLoader = false) => { try { if (showRefreshLoader) { setRefreshing(true) } else { setLoading(true) } setError(null) const response = await fetch(`${BaseUrl}/farmer/orders`, { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }) if (!response.ok) { if (response.status === 401) { throw new Error('Authentication failed. Please login again.') } throw new Error(`Failed to fetch orders: ${response.statusText}`) } const data = await response.json() setOrders(data.orders || []) } catch (err) { setError(err.message) console.error('Error fetching orders:', err) } finally { setLoading(false) setRefreshing(false) } }, [BaseUrl, token]) // Update order status const updateOrderStatus = async (orderId, status, note) => { if (!orderId || !status) { alert('Invalid order ID or status') return } try { setUpdatingStatus(true) const response = await fetch(`${BaseUrl}/farmer/orders/${orderId}/status`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status, note: note.trim() }) }) if (!response.ok) { if (response.status === 401) { throw new Error('Authentication failed. Please login again.') } const errorData = await response.json().catch(() => ({})) throw new Error(errorData.message || `Failed to update order status: ${response.statusText}`) } // Refresh orders after successful update await fetchOrders(true) setShowStatusModal(false) setNewStatus('') setStatusNote('') setSelectedOrder(null) } catch (err) { console.error('Error updating order status:', err) alert('Error updating order status: ' + err.message) } finally { setUpdatingStatus(false) } } useEffect(() => { if (token) { fetchOrders() } else { setError('No authentication token found') } }, [token, fetchOrders]) // Filter orders based on search and filters - memoized for performance const filteredOrders = useMemo(() => { return orders.filter(order => { const searchLower = searchTerm.toLowerCase() const matchesSearch = !searchTerm || order.customer?.name?.toLowerCase().includes(searchLower) || order.customer?.email?.toLowerCase().includes(searchLower) || order._id?.toLowerCase().includes(searchLower) || order.trackingNumber?.toLowerCase().includes(searchLower) const matchesStatus = statusFilter === 'all' || order.status === statusFilter const matchesPayment = paymentFilter === 'all' || order.paymentStatus === paymentFilter return matchesSearch && matchesStatus && matchesPayment }) }, [orders, searchTerm, statusFilter, paymentFilter]) // Pagination - memoized for performance const paginationData = useMemo(() => { const totalPages = Math.ceil(filteredOrders.length / ordersPerPage) const paginatedOrders = filteredOrders.slice( (currentPage - 1) * ordersPerPage, currentPage * ordersPerPage ) return { totalPages, paginatedOrders } }, [filteredOrders, currentPage, ordersPerPage]) // Reset to first page when filters change useEffect(() => { setCurrentPage(1) }, [searchTerm, statusFilter, paymentFilter]) // Status badge colors const getStatusColor = (status) => { const colors = { placed: 'bg-blue-100 text-blue-800 border-blue-200', processing: 'bg-yellow-100 text-yellow-800 border-yellow-200', dispatched: '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', default: 'bg-gray-100 text-gray-800 border-gray-200' } return colors[status] || colors.default } const getPaymentStatusColor = (status) => { const colors = { paid: 'bg-green-100 text-green-800 border-green-200', pending: 'bg-yellow-100 text-yellow-800 border-yellow-200', failed: 'bg-red-100 text-red-800 border-red-200', default: 'bg-gray-100 text-gray-800 border-gray-200' } return colors[status] || colors.default } // Get status icon const getStatusIcon = (status) => { switch (status) { case 'placed': return <Clock className="w-4 h-4" /> case 'processing': return <AlertCircle className="w-4 h-4" /> case 'dispatched': return <Truck className="w-4 h-4" /> case 'delivered': return <CheckCircle className="w-4 h-4" /> case 'cancelled': return <XCircle className="w-4 h-4" /> default: return <Package className="w-4 h-4" /> } } // Handle modal close const closeModals = () => { setShowDetailsModal(false) setShowStatusModal(false) setSelectedOrder(null) setNewStatus('') setStatusNote('') } // Handle status modal open const openStatusModal = (order) => { setSelectedOrder(order) setNewStatus(order.status) setShowStatusModal(true) } if (loading) { 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-green-600 mx-auto"></div> <p className="mt-4 text-gray-600">Loading orders...</p> </div> </div> ) } if (error) { return ( <div className="min-h-screen bg-gray-50 flex items-center justify-center"> <div className="text-center max-w-md"> <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" /> <h2 className="text-lg font-semibold text-gray-900 mb-2">Error Loading Orders</h2> <p className="text-red-600 mb-4">{error}</p> <button onClick={() => fetchOrders()} className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors" > Try Again </button> </div> </div> ) } return ( <div className="min-h-screen bg-gray-50 p-6"> <div className="max-w-7xl mx-auto"> <Stats /> {/* Header */} <div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between"> <div> <h1 className="text-2xl font-bold text-gray-900 mb-2">Orders Management</h1> <p className="text-gray-600">Manage and track all your orders</p> </div> <div className="mt-4 sm:mt-0 flex items-center gap-3"> <div className="text-sm text-gray-600"> Total: {filteredOrders.length} orders </div> <button onClick={() => fetchOrders(true)} disabled={refreshing} className="flex items-center gap-2 px-4 py-2 text-green-600 hover:text-green-700 hover:bg-green-50 rounded-lg transition-colors disabled:opacity-50" > <RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} /> Refresh </button> </div> </div> {/* Filters */} <div className="bg-white rounded-lg shadow-sm p-4 mb-6"> <div className="flex flex-col sm:flex-row gap-4"> {/* Search */} <div className="relative flex-1"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <input type="text" placeholder="Search orders, customers, tracking..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all" /> </div> {/* Status Filter */} <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 transition-all" > <option value="all">All Status</option> {statusOptions.map(status => ( <option key={status} value={status}> {status.charAt(0).toUpperCase() + status.slice(1)} </option> ))} </select> {/* Payment Filter */} <select value={paymentFilter} onChange={(e) => setPaymentFilter(e.target.value)} className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all" > <option value="all">All Payments</option> <option value="paid">Paid</option> <option value="pending">Pending</option> <option value="failed">Failed</option> </select> </div> </div> {/* Table */} <div className="bg-white rounded-lg shadow-sm overflow-hidden"> <div className="overflow-x-auto"> <table className="w-full"> <thead className="bg-gray-50 border-b border-gray-200"> <tr> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Order Info </th> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Customer </th> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Items </th> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Amount </th> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Status </th> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Payment </th> <th className="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Date </th> <th className="px-6 py-4 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> Actions </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {paginationData.paginatedOrders.length === 0 ? ( <tr> <td colSpan="8" className="px-6 py-12 text-center"> <Package className="w-12 h-12 text-gray-300 mx-auto mb-4" /> <p className="text-gray-500 font-medium">No orders found</p> {searchTerm || statusFilter !== 'all' || paymentFilter !== 'all' ? ( <p className="text-sm text-gray-400 mt-2"> Try adjusting your search or filters </p> ) : null} </td> </tr> ) : ( paginationData.paginatedOrders.map((order) => ( <tr key={order._id} className="hover:bg-gray-50 transition-colors"> {/* Order Info */} <td className="px-6 py-4 whitespace-nowrap"> <div> <div className="text-sm font-medium text-gray-900"> #{order._id?.slice(-8) || 'N/A'} </div> {order.trackingNumber && ( <div className="text-xs text-gray-500 mt-1"> Track: {order.trackingNumber} </div> )} </div> </td> {/* Customer */} <td className="px-6 py-4 whitespace-nowrap"> <div> <div className="text-sm font-medium text-gray-900"> {order.customer?.name || 'N/A'} </div> <div className="text-sm text-gray-500"> {order.customer?.email || 'N/A'} </div> {order.customer?.phone && ( <div className="text-xs text-gray-500 mt-1"> {order.customer.phone} </div> )} </div> </td> {/* Items */} <td className="px-6 py-4 whitespace-nowrap"> <div className="text-sm text-gray-900"> {order.items?.length || 0} item{(order.items?.length || 0) !== 1 ? 's' : ''} </div> <div className="text-xs text-gray-500 mt-1"> {order.items?.slice(0, 2).map((item, idx) => ( <div key={idx}> {item.product?.name || 'Unknown'} (×{item.quantity || 0}) </div> ))} {(order.items?.length || 0) > 2 && ( <div>+{order.items.length - 2} more</div> )} </div> </td> {/* Amount */} <td className="px-6 py-4 whitespace-nowrap"> <div className="text-sm font-medium text-gray-900"> ₹{(order.totalAmount || 0).toLocaleString('en-IN')} </div> </td> {/* Status */} <td className="px-6 py-4 whitespace-nowrap"> <div className="flex items-center gap-2"> <span className={`inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full border ${getStatusColor(order.status)}`}> {getStatusIcon(order.status)} {order.status?.charAt(0).toUpperCase() + order.status?.slice(1) || 'Unknown'} </span> </div> </td> {/* Payment */} <td className="px-6 py-4 whitespace-nowrap"> <span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full border ${getPaymentStatusColor(order.paymentStatus)}`}> {order.paymentStatus?.charAt(0).toUpperCase() + order.paymentStatus?.slice(1) || 'Unknown'} </span> </td> {/* Date */} <td className="px-6 py-4 whitespace-nowrap"> <div className="text-sm text-gray-900"> {order.createdAt ? new Date(order.createdAt).toLocaleDateString('en-IN') : 'N/A'} </div> <div className="text-xs text-gray-500 mt-1"> {order.createdAt ? new Date(order.createdAt).toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' }) : ''} </div> </td> {/* Actions */} <td className="px-6 py-4 whitespace-nowrap text-right"> <div className="flex items-center justify-end gap-2"> <button onClick={() => { setSelectedOrder(order) setShowDetailsModal(true) }} className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors" title="View Details" > <Eye className="w-4 h-4" /> </button> {/* {order.status !== 'delivered' && order.status !== 'cancelled' && ( <button onClick={() => openStatusModal(order)} className="p-2 text-green-600 hover:text-green-700 hover:bg-green-50 rounded-lg transition-colors" title="Update Status" > <Edit3 className="w-4 h-4" /> </button> )} */} </div> </td> </tr> )) )} </tbody> </table> </div> {/* Pagination */} {paginationData.totalPages > 1 && ( <div className="bg-white px-4 py-3 border-t border-gray-200 sm:px-6"> <div className="flex items-center justify-between"> <div className="flex-1 flex justify-between sm:hidden"> <button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > Previous </button> <button onClick={() => setCurrentPage(prev => Math.min(prev + 1, paginationData.totalPages))} disabled={currentPage === paginationData.totalPages} className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > Next </button> </div> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> <div> <p className="text-sm text-gray-700"> Showing <span className="font-medium">{(currentPage - 1) * ordersPerPage + 1}</span> to{' '} <span className="font-medium"> {Math.min(currentPage * ordersPerPage, filteredOrders.length)} </span>{' '} of <span className="font-medium">{filteredOrders.length}</span> results </p> </div> <div> <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"> <button onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))} disabled={currentPage === 1} className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > Previous </button> {Array.from({ length: Math.min(5, paginationData.totalPages) }, (_, i) => { const page = currentPage <= 3 ? i + 1 : currentPage - 2 + i if (page > paginationData.totalPages) return null return ( <button key={page} onClick={() => setCurrentPage(page)} className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium transition-colors ${ currentPage === page ? 'z-10 bg-green-50 border-green-500 text-green-600' : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50' }`} > {page} </button> ) })} <button onClick={() => setCurrentPage(prev => Math.min(prev + 1, paginationData.totalPages))} disabled={currentPage === paginationData.totalPages} className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > Next </button> </nav> </div> </div> </div> </div> )} </div> </div> {/* Order Details Modal */} {showDetailsModal && 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-4xl w-full max-h-[90vh] overflow-y-auto"> <div className="p-6 border-b border-gray-200"> <div className="flex items-center justify-between"> <h3 className="text-lg font-semibold text-gray-900"> Order Details - #{selectedOrder._id?.slice(-8) || 'N/A'} </h3> <button onClick={closeModals} className="text-gray-400 hover:text-gray-600 transition-colors" > <XCircle className="w-6 h-6" /> </button> </div> </div> <div className="p-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> {/* Customer & Shipping */} <div> <h4 className="font-medium text-gray-900 mb-4">Customer Information</h4> <div className="bg-gray-50 rounded-lg p-4 mb-4"> <p className="font-medium">{selectedOrder.customer?.name || 'N/A'}</p> <p className="text-sm text-gray-600">{selectedOrder.customer?.email || 'N/A'}</p> {selectedOrder.customer?.phone && ( <p className="text-sm text-gray-600">{selectedOrder.customer.phone}</p> )} </div> <h4 className="font-medium text-gray-900 mb-4">Shipping Address</h4> <div className="bg-gray-50 rounded-lg p-4"> <p className="text-sm text-gray-600"> {selectedOrder.shippingAddress?.street || 'N/A'}<br /> {selectedOrder.shippingAddress?.city || 'N/A'}, {selectedOrder.shippingAddress?.state || 'N/A'}<br /> {selectedOrder.shippingAddress?.zipCode || 'N/A'} </p> </div> </div> {/* Order Items */} <div> <h4 className="font-medium text-gray-900 mb-4">Order Items</h4> <div className="space-y-3"> {selectedOrder.items?.map((item, index) => ( <div key={index} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg"> <img src={item.product?.images?.[0]?.url || '/placeholder.jpg'} alt={item.product?.name || 'Product'} className="w-12 h-12 object-cover rounded-lg" onError={(e) => { e.target.src = '/placeholder.jpg' }} /> <div className="flex-1"> <p className="font-medium text-gray-900">{item.product?.name || 'Unknown Product'}</p> <p className="text-sm text-gray-600"> Qty: {item.quantity || 0} × ₹{item.price || 0} = ₹{((item.quantity || 0) * (item.price || 0)).toLocaleString('en-IN')} </p> </div> </div> )) || <p className="text-gray-500">No items found</p>} </div> <div className="mt-4 pt-4 border-t border-gray-200"> <div className="flex justify-between items-center"> <span className="font-medium text-gray-900">Total Amount:</span> <span className="font-bold text-lg text-gray-900"> ₹{(selectedOrder.totalAmount || 0).toLocaleString('en-IN')} </span> </div> </div> </div> </div> {/* Status History */} <div className="mt-6"> <h4 className="font-medium text-gray-900 mb-4">Status History</h4> <div className="space-y-3"> {selectedOrder.statusHistory?.length > 0 ? ( selectedOrder.statusHistory.map((status, index) => ( <div key={index} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg"> <div className="flex-shrink-0 mt-1"> <div className="w-2 h-2 bg-green-500 rounded-full"></div> </div> <div className="flex-1"> <div className="flex items-center gap-2 mb-1"> <span className="font-medium text-gray-900 capitalize">{status.status}</span> <span className="text-xs text-gray-500"> {status.timestamp ? new Date(status.timestamp).toLocaleString('en-IN') : 'N/A'} </span> </div> {status.note && <p className="text-sm text-gray-600">{status.note}</p>} </div> </div> )) ) : ( <p className="text-gray-500">No status history available</p> )} </div> </div> {/* Action Buttons */} <div className="mt-6 flex gap-3"> {selectedOrder.status !== 'delivered' && selectedOrder. status !== 'cancelled' && ( <button onClick={() => { setShowDetailsModal(false) openStatusModal(selectedOrder) }} className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2" > <Edit3 className="w-4 h-4" /> Update Status </button> )} <button onClick={closeModals} className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors" > Close </button> </div> </div> </div> </div> )} {/* Status Update Modal */} {showStatusModal && 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-md w-full"> <div className="p-6 border-b border-gray-200"> <div className="flex items-center justify-between"> <h3 className="text-lg font-semibold text-gray-900"> Update Order Status </h3> <button onClick={closeModals} className="text-gray-400 hover:text-gray-600 transition-colors" > <XCircle className="w-6 h-6" /> </button> </div> </div> <div className="p-6"> <div className="mb-4"> <p className="text-sm text-gray-600 mb-2"> Order: #{selectedOrder._id?.slice(-8) || 'N/A'} </p> <p className="text-sm text-gray-600"> Current Status: <span className="font-medium capitalize">{selectedOrder.status}</span> </p> </div> <div className="mb-4"> <label className="block text-sm font-medium text-gray-700 mb-2"> New Status </label> <select value={newStatus} onChange={(e) => setNewStatus(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all" > {statusOptions.map(status => ( <option key={status} value={status}> {status.charAt(0).toUpperCase() + status.slice(1)} </option> ))} </select> </div> <div className="mb-6"> <label className="block text-sm font-medium text-gray-700 mb-2"> Note (Optional) </label> <textarea value={statusNote} onChange={(e) => setStatusNote(e.target.value)} placeholder="Add a note about this status change..." rows={3} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all resize-none" /> </div> <div className="flex gap-3"> <button onClick={() => updateOrderStatus(selectedOrder._id, newStatus, statusNote)} disabled={updatingStatus || !newStatus} className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2" > {updatingStatus ? ( <> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> Updating... </> ) : ( <> <CheckCircle className="w-4 h-4" /> Update Status </> )} </button> <button onClick={closeModals} disabled={updatingStatus} className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors disabled:opacity-50" > Cancel </button> </div> </div> </div> </div> )} </div> ) } export default FramersMyOrders