diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js index 645c8fc..8b189a7 100644 --- a/backend/src/routes/products.js +++ b/backend/src/routes/products.js @@ -432,19 +432,15 @@ router.put('/user/:id', validateParams(idParamSchema), validateRequest(userProdu customRateUnit, notes, isAdvancedEdit, - // Advanced edit fields (will be ignored for now as schema doesn't support them) + // Advanced edit fields brand, categoryId, productType, activeIngredients, - description + description, + spreaderSettings } = req.body; - // Log warning if advanced edit is attempted (not fully supported yet) - if (isAdvancedEdit) { - console.warn(`Advanced edit attempted for user product ${userProductId}. Only basic fields will be updated.`); - } - // Check if user product exists and belongs to user const checkResult = await pool.query( 'SELECT id FROM user_products WHERE id = $1 AND user_id = $2', @@ -491,6 +487,32 @@ router.put('/user/:id', validateParams(idParamSchema), validateRequest(userProdu const userProduct = result.rows[0]; + // Handle spreader settings for granular products + if (spreaderSettings && Array.isArray(spreaderSettings) && productType === 'granular') { + // First, delete existing spreader settings for this user product + await pool.query( + 'DELETE FROM product_spreader_settings WHERE user_product_id = $1', + [userProductId] + ); + + // Then add the new settings + for (const setting of spreaderSettings) { + await pool.query( + `INSERT INTO product_spreader_settings + (user_product_id, spreader_brand, spreader_model, setting_value, rate_description, notes) + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + userProductId, + setting.spreaderBrand, + setting.spreaderModel, + setting.settingValue, + setting.rateDescription, + setting.notes + ] + ); + } + } + res.json({ success: true, message: 'Custom product updated successfully', diff --git a/docker-compose.yml b/docker-compose.yml index 20132a3..76bf687 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: - "traefik.http.routers.turftracker-frontend.tls.certresolver=letsencrypt" - "traefik.http.services.turftracker-frontend.loadbalancer.server.port=3000" - "traefik.docker.network=proxy" + - "traefik.constraint=proxy-public" restart: unless-stopped backend: diff --git a/frontend/src/pages/Products/Products.js b/frontend/src/pages/Products/Products.js index 257333a..22f86c2 100644 --- a/frontend/src/pages/Products/Products.js +++ b/frontend/src/pages/Products/Products.js @@ -80,7 +80,7 @@ const Products = () => { spreaderModel: setting.spreaderModel || null, settingValue: setting.settingValue, rateDescription: setting.rateDescription || null, - notes: setting.notes || null + notes: setting.notes && setting.notes.trim() ? setting.notes.trim() : null }) ); @@ -717,7 +717,7 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories }) ); }; -// Edit Product Modal Component +// Edit Product Modal Component const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categories }) => { const [formData, setFormData] = useState({ productId: product.baseProductId || '', @@ -734,6 +734,41 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor }); const [editMode, setEditMode] = useState('basic'); // 'basic' or 'advanced' + const [editSpreaderSettings, setEditSpreaderSettings] = useState([]); + const [newEditSpreaderSetting, setNewEditSpreaderSetting] = useState({ + spreaderBrand: '', + spreaderModel: '', + settingValue: '', + rateDescription: '', + notes: '' + }); + const [loadingSettings, setLoadingSettings] = useState(false); + + // Load existing spreader settings when modal opens + useEffect(() => { + const loadSpreaderSettings = async () => { + if (product.productType === 'granular' && product.id) { + setLoadingSettings(true); + try { + const response = await fetch(`/api/product-spreader-settings/user-product/${product.id}`, { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('token')}` + } + }); + if (response.ok) { + const data = await response.json(); + setEditSpreaderSettings(data.data || []); + } + } catch (error) { + console.error('Failed to load spreader settings:', error); + } finally { + setLoadingSettings(false); + } + } + }; + + loadSpreaderSettings(); + }, [product.id, product.productType]); const handleSubmit = (e) => { e.preventDefault(); @@ -774,20 +809,51 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor customName: formData.customName, customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null, customRateUnit: formData.customRateUnit || null, - notes: formData.notes || null, + notes: formData.notes && formData.notes.trim() ? formData.notes.trim() : null, // Additional fields for advanced mode brand: formData.brand || null, categoryId: formData.categoryId ? parseInt(formData.categoryId) : null, productType: formData.productType || null, activeIngredients: formData.activeIngredients || null, description: formData.description || null, - isAdvancedEdit: true // Flag to indicate this is an advanced edit + isAdvancedEdit: true, // Flag to indicate this is an advanced edit + // Include spreader settings for granular products + ...(formData.productType === 'granular' && editSpreaderSettings.length > 0 && { + spreaderSettings: editSpreaderSettings.map(setting => ({ + spreaderBrand: setting.spreaderBrand, + spreaderModel: setting.spreaderModel && setting.spreaderModel.trim() ? setting.spreaderModel.trim() : null, + settingValue: setting.settingValue, + rateDescription: setting.rateDescription && setting.rateDescription.trim() ? setting.rateDescription.trim() : null, + notes: setting.notes && setting.notes.trim() ? setting.notes.trim() : null + })) + }) }; onSubmit(submitData); } }; + // Handler functions for edit spreader settings + const handleAddEditSpreaderSetting = () => { + if (!newEditSpreaderSetting.spreaderBrand.trim() || !newEditSpreaderSetting.settingValue.trim()) { + return; + } + + setEditSpreaderSettings([...editSpreaderSettings, { ...newEditSpreaderSetting }]); + setNewEditSpreaderSetting({ + spreaderBrand: '', + spreaderModel: '', + settingValue: '', + rateDescription: '', + notes: '' + }); + }; + + const handleRemoveExistingSpreaderSetting = (index) => { + const updated = editSpreaderSettings.filter((_, i) => i !== index); + setEditSpreaderSettings(updated); + }; + return (
@@ -1013,6 +1079,101 @@ const EditProductModal = ({ product, onSubmit, onCancel, sharedProducts, categor placeholder="Special mixing instructions, storage notes, etc." />
+ + {formData.productType === 'granular' && ( +
+ +
+ {/* Existing spreader settings */} + {editSpreaderSettings.length > 0 && ( +
+

Current Settings:

+ {editSpreaderSettings.map((setting, index) => ( +
+
+
+
{setting.spreaderBrand} {setting.spreaderModel && `(${setting.spreaderModel})`}
+
Setting: {setting.settingValue}
+ {setting.rateDescription &&
{setting.rateDescription}
} + {setting.notes &&
{setting.notes}
} +
+ +
+
+ ))} +
+ )} + + {/* Add new spreader setting */} +
+

Add New Setting:

+
+
+ setNewEditSpreaderSetting({ ...newEditSpreaderSetting, spreaderBrand: e.target.value })} + /> +
+
+ setNewEditSpreaderSetting({ ...newEditSpreaderSetting, spreaderModel: e.target.value })} + /> +
+
+
+
+ setNewEditSpreaderSetting({ ...newEditSpreaderSetting, settingValue: e.target.value })} + /> +
+
+ setNewEditSpreaderSetting({ ...newEditSpreaderSetting, rateDescription: e.target.value })} + /> +
+
+
+