UNPKG

ajinkya-mhetre-mern

Version:

A MERN starter with frontend and backend folders

588 lines (550 loc) 25.4 kB
import React, { useState, useEffect } from 'react'; import { Plus, Edit3, Save, X, Upload, Eye, Package, Trash2 } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import Stats from '../../components/Framers/Stats'; const baseUrl = import.meta.env.VITE_API_URL; function FarmersAddProducts() { const { token } = useAuth(); const [products, setProducts] = useState([]); const [showAddForm, setShowAddForm] = useState(false); const [editingProduct, setEditingProduct] = useState(null); const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState('view'); // Form state const [formData, setFormData] = useState({ name: '', description: '', category: 'fruit', price: '', unit: 'kg', availableQuantity: '', harvestDate: '', expiryDate: '', images: [] }); // Edit form state const [editData, setEditData] = useState({ price: '', availableQuantity: '', description: '' }); const categories = ['fruit', 'vegetable', 'grain', 'dairy', 'meat', 'herbs']; const units = ['kg', 'dozen', 'piece', 'bunch', 'liter']; useEffect(() => { fetchProducts(); }, []); const fetchProducts = async () => { try { setLoading(true); const response = await fetch(`${baseUrl}/farmer/products`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { setProducts(data.products); } } catch (error) { console.error('Error fetching products:', error); setProducts([]); // Set empty array on error } finally { setLoading(false); } }; const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleEditInputChange = (e) => { const { name, value } = e.target; setEditData(prev => ({ ...prev, [name]: value })); }; const handleImageUpload = (e) => { const files = Array.from(e.target.files); setFormData(prev => ({ ...prev, images: files })); }; const handleSubmit = async () => { try { setLoading(true); // Create FormData for file upload const submitData = new FormData(); Object.keys(formData).forEach(key => { if (key === 'images') { formData.images.forEach(file => { submitData.append('images', file); }); } else { submitData.append(key, formData[key]); } }); const response = await fetch(`${baseUrl}/farmer/products`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: submitData }); const data = await response.json(); if (data.success) { fetchProducts(); // Refresh the list resetForm(); setActiveTab('view'); } } catch (error) { console.error('Error adding product:', error); } finally { setLoading(false); } }; const handleUpdate = async (productId) => { try { setLoading(true); const response = await fetch(`${baseUrl}/farmer/products/${productId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(editData) }); const data = await response.json(); if (data.success) { fetchProducts(); // Refresh the list setEditingProduct(null); setEditData({ price: '', availableQuantity: '', description: '' }); } } catch (error) { console.error('Error updating product:', error); } finally { setLoading(false); } }; const startEdit = (product) => { setEditingProduct(product._id); setEditData({ price: product.price, availableQuantity: product.availableQuantity, description: product.description }); }; const cancelEdit = () => { setEditingProduct(null); setEditData({ price: '', availableQuantity: '', description: '' }); }; const resetForm = () => { setFormData({ name: '', description: '', category: 'fruit', price: '', unit: 'kg', availableQuantity: '', harvestDate: '', expiryDate: '', images: [] }); }; const formatDate = (dateString) => { return new Date(dateString).toLocaleDateString(); }; return ( <div className="min-h-screen bg-gray-50 p-6"> <div className="max-w-7xl mx-auto"> <Stats /> {/* Header */} <div className="border-b border-gray-200 p-6"> <h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3"> <Package className="text-green-600" /> Products Management </h1> <p className="text-gray-600 mt-2">Manage your farm products, pricing, and inventory</p> </div> {/* Tabs */} <div className="border-b border-gray-200"> <nav className="flex space-x-8 px-6"> <button onClick={() => setActiveTab('view')} className={`py-4 px-1 border-b-2 font-medium text-sm ${ activeTab === 'view' ? 'border-green-500 text-green-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > <Eye className="inline mr-2" size={16} /> View Products </button> <button onClick={() => setActiveTab('add')} className={`py-4 px-1 border-b-2 font-medium text-sm ${ activeTab === 'add' ? 'border-green-500 text-green-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }`} > <Plus className="inline mr-2" size={16} /> Add Product </button> </nav> </div> {/* Content */} <div className="p-6"> {activeTab === 'view' && ( <div className="space-y-6"> <div className="flex justify-between items-center"> <h2 className="text-xl font-semibold text-gray-900">Your Products</h2> <div className="text-sm text-gray-600"> Total Products: {products.length} </div> </div> {loading ? ( <div className="text-center py-12"> <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 products...</p> </div> ) : products.length === 0 ? ( <div className="text-center py-12"> <Package className="mx-auto h-12 w-12 text-gray-400" /> <h3 className="mt-2 text-sm font-medium text-gray-900">No products</h3> <p className="mt-1 text-sm text-gray-500">Get started by adding your first product.</p> <div className="mt-6"> <button onClick={() => setActiveTab('add')} className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700" > <Plus className="mr-2" size={16} /> Add Product </button> </div> </div> ) : ( <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Product </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Category </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Price </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Quantity </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Harvest Date </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Expiry Date </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Status </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Actions </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {products.map((product) => ( <tr key={product._id} className="hover:bg-gray-50"> <td className="px-6 py-4 whitespace-nowrap"> <div className="flex items-center"> <div className="flex-shrink-0 h-10 w-10"> {product.images && product.images.length > 0 ? ( <img className="h-10 w-10 rounded-full object-cover" src={product.images[0].url} alt={product.name} /> ) : ( <div className="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center"> <Package size={16} className="text-gray-400" /> </div> )} </div> <div className="ml-4"> <div className="text-sm font-medium text-gray-900">{product.name}</div> <div className="text-sm text-gray-500 max-w-xs truncate"> {editingProduct === product._id ? ( <input type="text" name="description" value={editData.description} onChange={handleEditInputChange} className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm" /> ) : ( product.description )} </div> </div> </div> </td> <td className="px-6 py-4 whitespace-nowrap"> <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800 capitalize"> {product.category} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {editingProduct === product._id ? ( <div className="flex items-center"> <span className="mr-1"></span> <input type="number" name="price" value={editData.price} onChange={handleEditInputChange} className="w-20 border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm" /> <span className="ml-1 text-gray-500">/{product.unit}</span> </div> ) : ( `₹${product.price}/${product.unit}` )} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {editingProduct === product._id ? ( <div className="flex items-center"> <input type="number" name="availableQuantity" value={editData.availableQuantity} onChange={handleEditInputChange} className="w-20 border-gray-300 rounded-md shadow-sm focus:ring-green-500 focus:border-green-500 sm:text-sm" /> <span className="ml-1 text-gray-500">{product.unit}</span> </div> ) : ( `${product.availableQuantity} ${product.unit}` )} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {formatDate(product.harvestDate)} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {formatDate(product.expiryDate)} </td> <td className="px-6 py-4 whitespace-nowrap"> <div className="flex flex-col space-y-1"> <span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${ product.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }`}> {product.isActive ? 'Active' : 'Inactive'} </span> {product.availableQuantity === 0 && ( <span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-orange-100 text-orange-800"> Out of Stock </span> )} </div> </td> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> {editingProduct === product._id ? ( <div className="flex space-x-2"> <button onClick={() => handleUpdate(product._id)} disabled={loading} className="text-green-600 hover:text-green-900 disabled:opacity-50" > <Save size={16} /> </button> <button onClick={cancelEdit} className="text-gray-600 hover:text-gray-900" > <X size={16} /> </button> </div> ) : ( <button onClick={() => startEdit(product)} className="text-blue-600 hover:text-blue-900" > <Edit3 size={16} /> </button> )} </td> </tr> ))} </tbody> </table> </div> )} </div> )} {activeTab === 'add' && ( <div className="max-w-2xl mx-auto"> <h2 className="text-xl font-semibold text-gray-900 mb-6">Add New Product</h2> <div className="space-y-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Product Name</label> <input type="text" name="name" value={formData.name} onChange={handleInputChange} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="e.g., Fresh Tomatoes" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Category</label> <select name="category" value={formData.category} onChange={handleInputChange} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" > {categories.map(category => ( <option key={category} value={category}> {category.charAt(0).toUpperCase() + category.slice(1)} </option> ))} </select> </div> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Description</label> <textarea name="description" value={formData.description} onChange={handleInputChange} rows={3} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" placeholder="Describe your product..." /> </div> <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Price (₹)</label> <input type="number" name="price" value={formData.price} onChange={handleInputChange} min="0" step="0.01" className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Unit</label> <select name="unit" value={formData.unit} onChange={handleInputChange} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" > {units.map(unit => ( <option key={unit} value={unit}>{unit}</option> ))} </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Available Quantity</label> <input type="number" name="availableQuantity" value={formData.availableQuantity} onChange={handleInputChange} min="0" className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" /> </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Harvest Date</label> <input type="date" name="harvestDate" value={formData.harvestDate} onChange={handleInputChange} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Expiry Date</label> <input type="date" name="expiryDate" value={formData.expiryDate} onChange={handleInputChange} className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500 focus:border-transparent" /> </div> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2">Product Images</label> <div className="border-2 border-dashed border-gray-300 rounded-md p-6 text-center hover:border-green-500 transition-colors"> <Upload className="mx-auto h-12 w-12 text-gray-400" /> <div className="mt-4"> <label htmlFor="images" className="cursor-pointer"> <span className="mt-2 block text-sm font-medium text-gray-900"> Upload product images </span> <input id="images" name="images" type="file" multiple accept="image/*" onChange={handleImageUpload} className="sr-only" /> </label> <p className="mt-1 text-xs text-gray-500">PNG, JPG, GIF up to 10MB each</p> </div> </div> {formData.images.length > 0 && ( <div className="mt-2 text-sm text-gray-600"> {formData.images.length} file(s) selected </div> )} </div> <div className="flex gap-4 pt-6"> <button onClick={handleSubmit} disabled={loading} className="flex-1 flex items-center justify-center gap-2 px-6 py-3 bg-green-600 text-white rounded-md hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors disabled:opacity-50" > {loading ? ( <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> ) : ( <Plus size={16} /> )} Add Product </button> <button onClick={resetForm} className="px-6 py-3 border border-gray-300 text-gray-700 rounded-md hover:bg-gray-50 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors" > Reset </button> </div> </div> </div> )} </div> </div> </div> ); } export default FarmersAddProducts;