import React, { useState, useEffect } from 'react'; import { PlusIcon, MagnifyingGlassIcon, FunnelIcon, BeakerIcon, TrashIcon, PencilIcon } from '@heroicons/react/24/outline'; import { productsAPI, productSpreaderSettingsAPI, equipmentAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import toast from 'react-hot-toast'; const Products = () => { const [sharedProducts, setSharedProducts] = useState([]); const [userProducts, setUserProducts] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [showCreateForm, setShowCreateForm] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [selectedType, setSelectedType] = useState(''); const [activeTab, setActiveTab] = useState('shared'); // 'shared' or 'custom' const [editingProduct, setEditingProduct] = useState(null); const [showEditForm, setShowEditForm] = useState(false); useEffect(() => { fetchData(); }, []); const fetchData = async () => { try { setLoading(true); const [productsResponse, categoriesResponse] = await Promise.all([ productsAPI.getAll({ category: selectedCategory, type: selectedType, search: searchTerm }), productsAPI.getCategories() ]); setSharedProducts(productsResponse.data.data.sharedProducts || []); setUserProducts(productsResponse.data.data.userProducts || []); setCategories(categoriesResponse.data.data.categories || []); } catch (error) { console.error('Failed to fetch products:', error); toast.error('Failed to load products'); setSharedProducts([]); setUserProducts([]); } finally { setLoading(false); } }; const handleSearch = (e) => { setSearchTerm(e.target.value); // Debounce search setTimeout(() => { fetchData(); }, 300); }; const handleFilterChange = () => { fetchData(); }; const handleCreateProduct = async (productData) => { try { // Create the product first const response = await productsAPI.createUserProduct(productData); const createdProduct = response.data.data.userProduct; // Save spreader settings if any if (productData.spreaderSettings && productData.spreaderSettings.length > 0) { const settingPromises = productData.spreaderSettings.map(setting => { const payload = { userProductId: createdProduct.id, settingValue: setting.settingValue, rateDescription: setting.rateDescription || null, notes: setting.notes && setting.notes.trim() ? setting.notes.trim() : null }; // Use equipment-based approach if equipmentId is available if (setting.equipmentId) { payload.equipmentId = parseInt(setting.equipmentId); } else { // Fall back to legacy approach payload.spreaderBrand = setting.spreaderBrand; payload.spreaderModel = setting.spreaderModel || null; } return productSpreaderSettingsAPI.create(payload); }); await Promise.all(settingPromises); } toast.success(`Custom product created successfully${productData.spreaderSettings?.length ? ` with ${productData.spreaderSettings.length} spreader setting(s)` : ''}!`); setShowCreateForm(false); fetchData(); } catch (error) { console.error('Failed to create product:', error); toast.error('Failed to create product'); } }; const handleDeleteUserProduct = async (productId) => { if (window.confirm('Are you sure you want to delete this custom product?')) { try { await productsAPI.deleteUserProduct(productId); toast.success('Product deleted successfully'); fetchData(); } catch (error) { console.error('Failed to delete product:', error); toast.error('Failed to delete product'); } } }; const handleEditProduct = (product) => { setEditingProduct(product); setShowEditForm(true); }; const handleUpdateProduct = async (productData) => { try { await productsAPI.updateUserProduct(editingProduct.id, productData); toast.success('Product updated successfully!'); setShowEditForm(false); setEditingProduct(null); fetchData(); } catch (error) { console.error('Failed to update product:', error); toast.error('Failed to update product'); } }; const handleAddToMyProducts = async (sharedProduct) => { try { // Create a custom product based on the shared product const productData = { productId: sharedProduct.id, // Link to the shared product customName: sharedProduct.name, // Use the shared product's name as default brand: sharedProduct.brand, categoryId: null, // Will use the shared product's category productType: sharedProduct.productType, activeIngredients: sharedProduct.activeIngredients, description: sharedProduct.description, // Set default rate if available customRateAmount: sharedProduct.rates?.[0]?.rateAmount || null, customRateUnit: sharedProduct.rates?.[0]?.rateUnit || (sharedProduct.productType === 'granular' ? 'lbs/1000 sq ft' : 'oz/1000 sq ft'), notes: `Added from shared product: ${sharedProduct.name}` }; const response = await productsAPI.createUserProduct(productData); toast.success(`"${sharedProduct.name}" added to your products!`); // Refresh the data to show the new custom product fetchData(); } catch (error) { console.error('Failed to add product to my products:', error); toast.error('Failed to add product to your collection'); } }; const ProductCard = ({ product, isUserProduct = false, onAddToMyProducts }) => (

{isUserProduct ? product.customName || product.baseProductName : product.name}

{product.brand && (

{product.brand}

)} {product.productType ? product.productType.charAt(0).toUpperCase() + product.productType.slice(1) : 'Type not set'}
{isUserProduct && (
)}
{product.categoryName && (

Category: {product.categoryName}

)} {/* Seed blend pretty display */} {product.productType === 'seed' ? ( (()=>{ let blend = []; // Shared product if (Array.isArray(product.seedBlend) && product.seedBlend.length) { blend = product.seedBlend; } else if (product.activeIngredients) { // User product stores blend in activeIngredients JSON try { const ai = typeof product.activeIngredients === 'string' ? JSON.parse(product.activeIngredients) : product.activeIngredients; blend = ai?.seedBlend || []; } catch { /* ignore */ } } return (

Seed Blend:

{blend.length === 0 ? (

No blend details

) : (
{blend.map((b, idx)=> ( {b.cultivar} — {parseFloat(b.percent || 0).toFixed(1)}% ))}
)}
); })() ) : ( product.activeIngredients && (

Active Ingredients: {product.activeIngredients}

) )} {/* Application Rates */} {isUserProduct && ( (()=>{ if (product.productType === 'seed') { try { const ai = typeof product.activeIngredients === 'string' ? JSON.parse(product.activeIngredients) : product.activeIngredients; const sr = ai?.seedRates; if (sr?.new || sr?.overseed) { return (

Your Rates:

{sr.new &&

New Lawn: {sr.new} {sr.unit || product.customRateUnit}

} {sr.overseed &&

Overseeding: {sr.overseed} {sr.unit || product.customRateUnit}

}
); } } catch {} } if (product.customRateAmount) { return (

Your Rate:

{product.customRateAmount} {product.customRateUnit}

); } return null; })() )} {!isUserProduct && product.rates && product.rates.length > 0 && (

Application Rates:

{product.productType === 'seed' ? ( (()=>{ const newRate = product.rates.find(r=> (r.applicationType||'').toLowerCase().includes('new')); const overRate = product.rates.find(r=> (r.applicationType||'').toLowerCase().includes('over')); return ( <> {newRate && (
New Lawn: {newRate.rateAmount} {newRate.rateUnit}
)} {overRate && (
Overseeding: {overRate.rateAmount} {overRate.rateUnit}
)} {!newRate && !overRate && product.rates.slice(0,2).map((rate, index)=> (
{rate.applicationType}: {rate.rateAmount} {rate.rateUnit}
))} ); })() ) : ( product.rates.slice(0, 2).map((rate, index) => (
{rate.applicationType}: {rate.rateAmount} {rate.rateUnit}
)) )} {product.rates.length > 2 && (

+{product.rates.length - 2} more rates

)}
)} {product.notes && (

Notes: {product.notes}

)} {!isUserProduct && (
)}
); // Avoid blowing away the modal while background loading occurs if (loading && !showCreateForm && !showEditForm) { return (
); } return (

Products

Manage your lawn care products and application rates

{/* Search and Filters */}
{/* Search */}
{/* Category Filter */}
{/* Type Filter */}
{/* Tabs */}
{/* Products Grid */} {activeTab === 'shared' ? ( sharedProducts.length === 0 ? (

No Products Found

Try adjusting your search or filters

) : (
{sharedProducts.map((product) => ( ))}
) ) : ( userProducts.length === 0 ? (

No Custom Products Yet

Create your own products with custom rates and notes

) : (
{userProducts.map((product) => ( ))}
) )} {/* Create Product Form Modal */} {showCreateForm && ( setShowCreateForm(false)} sharedProducts={sharedProducts} categories={categories} /> )} {/* Edit Product Form Modal */} {showEditForm && editingProduct && ( { setShowEditForm(false); setEditingProduct(null); }} sharedProducts={sharedProducts} categories={categories} /> )}
); }; // Create Product Modal Component const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories }) => { const [formData, setFormData] = useState({ productId: '', customName: '', categoryId: '', productType: 'granular', brand: '', activeIngredients: '', customRateAmount: '', customRateUnit: 'lbs/1000 sq ft', notes: '' }); const [spreaderSettings, setSpreaderSettings] = useState([]); const [seedBlend, setSeedBlend] = useState([]); // [{cultivar:'', percent:0}] const [seedNewRate, setSeedNewRate] = useState(''); const [seedOverRate, setSeedOverRate] = useState(''); const [availableSpreaders, setAvailableSpreaders] = useState([]); const [loadingSpreaders, setLoadingSpreaders] = useState(false); const [newSpreaderSetting, setNewSpreaderSetting] = useState({ equipmentId: '', settingValue: '', rateDescription: '', notes: '' }); // Load user's spreader equipment useEffect(() => { const loadSpreaders = async () => { setLoadingSpreaders(true); try { const response = await equipmentAPI.getSpreaders(); setAvailableSpreaders(response.data.data.spreaders || []); } catch (error) { console.error('Failed to load spreaders:', error); toast.error('Failed to load spreader equipment'); } finally { setLoadingSpreaders(false); } }; loadSpreaders(); }, []); const addSpreaderSetting = () => { if (!newSpreaderSetting.equipmentId || !newSpreaderSetting.settingValue) { toast.error('Please select spreader equipment and enter setting value'); return; } const selectedSpreader = availableSpreaders.find(s => s.id === parseInt(newSpreaderSetting.equipmentId)); const settingWithSpreaderInfo = { ...newSpreaderSetting, equipmentId: parseInt(newSpreaderSetting.equipmentId), // Convert to number id: Date.now(), equipmentName: selectedSpreader?.name, equipmentManufacturer: selectedSpreader?.manufacturer, equipmentModel: selectedSpreader?.model }; setSpreaderSettings([...spreaderSettings, settingWithSpreaderInfo]); setNewSpreaderSetting({ equipmentId: '', settingValue: '', rateDescription: '', notes: '' }); }; const removeSpreaderSetting = (id) => { setSpreaderSettings(spreaderSettings.filter(setting => setting.id !== id)); }; const handleSubmit = (e) => { e.preventDefault(); if (!formData.productId && !formData.customName) { toast.error('Please select a base product or enter a custom name'); return; } // If creating a completely custom product (no base product), require more fields if (!formData.productId && (!formData.categoryId || !formData.productType)) { toast.error('Please select a category and product type for custom products'); return; } const submitData = { productId: formData.productId ? parseInt(formData.productId) : null, customName: formData.customName || null, customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null, customRateUnit: formData.customRateUnit || null, notes: formData.notes || null, // Advanced fields for custom products brand: formData.brand || null, categoryId: formData.categoryId ? parseInt(formData.categoryId) : null, productType: formData.productType || null, activeIngredients: formData.activeIngredients || null, description: formData.description || null, spreaderSettings: formData.productType === 'granular' ? spreaderSettings : [] }; // If seed, include blend and optional seed rates in activeIngredients JSON if (formData.productType === 'seed') { const payload = { seedBlend: Array.isArray(seedBlend) ? seedBlend : [] }; if (seedNewRate || seedOverRate) { payload.seedRates = { new: seedNewRate ? parseFloat(seedNewRate) : null, overseed: seedOverRate ? parseFloat(seedOverRate) : null, unit: formData.customRateUnit }; } submitData.activeIngredients = JSON.stringify(payload); } onSubmit(submitData); }; return (

Add Custom Product

Choose from our database or create a completely custom product below

setFormData({ ...formData, customName: e.target.value })} placeholder="e.g., My 24-0-11 Blend, Custom Herbicide Mix" />

Required if no base product selected. Used to identify this product.

{/* Show additional fields when creating completely custom product */} {!formData.productId && ( <>
{formData.productType === 'seed' && ( <>
setSeedNewRate(e.target.value)} placeholder="e.g., 7" />
setSeedOverRate(e.target.value)} placeholder="e.g., 3" />
)}
setFormData({ ...formData, brand: e.target.value })} placeholder="e.g., Scotts, Syngenta, Custom Mix" />
setFormData({ ...formData, activeIngredients: e.target.value })} placeholder="e.g., 2,4-D 25%, Nitrogen 24%, Iron 2%" />
)}
setFormData({ ...formData, customRateAmount: e.target.value })} placeholder="2.5" />
{/* Spreader Settings for Granular Products */} {formData.productType === 'granular' && (

Spreader Settings

Add spreader settings for different brands/models. This helps determine the correct spreader dial setting when applying this product.

{/* List existing spreader settings */} {spreaderSettings.length > 0 && (
{spreaderSettings.map((setting) => (
{setting.equipmentName || `${setting.equipmentManufacturer} ${setting.equipmentModel}`.trim()} - Setting: {setting.settingValue}
{setting.rateDescription && (
{setting.rateDescription}
)}
))}
)} {/* Add new spreader setting form */}
Add Spreader Setting
{availableSpreaders.length === 0 ? (

No spreader equipment found. Please add spreader equipment to your inventory first.

) : ( <>
)} {availableSpreaders.length > 0 && (
setNewSpreaderSetting({ ...newSpreaderSetting, settingValue: e.target.value })} placeholder="#14, 4, 20, etc." />
setNewSpreaderSetting({ ...newSpreaderSetting, rateDescription: e.target.value })} placeholder="e.g., 1 lb N per 1000 sq ft" />
)} {availableSpreaders.length > 0 && (