From 229454c4662ec3155455ab7e5897e83460de4656 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Sun, 24 Aug 2025 13:41:12 -0400 Subject: [PATCH] spreader stuff --- backend/src/app.js | 4 + backend/src/routes/productSpreaderSettings.js | 241 ++++++++++++++++++ backend/src/routes/spreaderSettings.js | 93 +++++++ backend/src/utils/applicationCalculations.js | 14 +- database/migrations/add_spreader_settings.sql | 31 +++ database/migrations/user_spreader_system.sql | 56 ++++ .../src/pages/Applications/Applications.js | 28 +- frontend/src/pages/Equipment/Equipment.js | 1 + frontend/src/pages/Products/Products.js | 149 ++++++++++- frontend/src/services/api.js | 16 ++ 10 files changed, 616 insertions(+), 17 deletions(-) create mode 100644 backend/src/routes/productSpreaderSettings.js create mode 100644 backend/src/routes/spreaderSettings.js create mode 100644 database/migrations/add_spreader_settings.sql create mode 100644 database/migrations/user_spreader_system.sql diff --git a/backend/src/app.js b/backend/src/app.js index 7e3b1a3..807a468 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -13,6 +13,8 @@ const equipmentRoutes = require('./routes/equipment'); const nozzleRoutes = require('./routes/nozzles'); const productRoutes = require('./routes/products'); const applicationRoutes = require('./routes/applications'); +const spreaderSettingsRoutes = require('./routes/spreaderSettings'); +const productSpreaderSettingsRoutes = require('./routes/productSpreaderSettings'); const weatherRoutes = require('./routes/weather'); const adminRoutes = require('./routes/admin'); @@ -86,6 +88,8 @@ app.use('/api/equipment', authenticateToken, equipmentRoutes); app.use('/api/nozzles', authenticateToken, nozzleRoutes); app.use('/api/products', authenticateToken, productRoutes); app.use('/api/applications', authenticateToken, applicationRoutes); +app.use('/api/spreader-settings', authenticateToken, spreaderSettingsRoutes); +app.use('/api/product-spreader-settings', authenticateToken, productSpreaderSettingsRoutes); app.use('/api/weather', authenticateToken, weatherRoutes); app.use('/api/admin', authenticateToken, adminRoutes); diff --git a/backend/src/routes/productSpreaderSettings.js b/backend/src/routes/productSpreaderSettings.js new file mode 100644 index 0000000..778c248 --- /dev/null +++ b/backend/src/routes/productSpreaderSettings.js @@ -0,0 +1,241 @@ +const express = require('express'); +const pool = require('../config/database'); +const { validateRequest, validateParams } = require('../utils/validation'); +const { AppError } = require('../middleware/errorHandler'); +const Joi = require('joi'); + +const router = express.Router(); + +// Validation schemas +const spreaderSettingSchema = Joi.object({ + productId: Joi.number().integer().positive().optional(), + userProductId: Joi.number().integer().positive().optional(), + spreaderBrand: Joi.string().max(100).required(), + spreaderModel: Joi.string().max(100).optional(), + settingValue: Joi.string().max(20).required(), + rateDescription: Joi.string().max(200).optional(), + notes: Joi.string().optional() +}).xor('productId', 'userProductId'); // Must have either productId or userProductId, but not both + +const idParamSchema = Joi.object({ + id: Joi.number().integer().positive().required() +}); + +// @route GET /api/product-spreader-settings/product/:productId +// @desc Get spreader settings for a specific product +// @access Private +router.get('/product/:productId', validateParams(idParamSchema), async (req, res, next) => { + try { + const productId = req.params.productId; + + const result = await pool.query( + `SELECT * FROM product_spreader_settings + WHERE product_id = $1 + ORDER BY spreader_brand, spreader_model NULLS LAST, setting_value`, + [productId] + ); + + res.json({ + success: true, + data: { + settings: result.rows.map(row => ({ + id: row.id, + spreaderBrand: row.spreader_brand, + spreaderModel: row.spreader_model, + settingValue: row.setting_value, + rateDescription: row.rate_description, + notes: row.notes, + createdAt: row.created_at + })) + } + }); + } catch (error) { + next(error); + } +}); + +// @route GET /api/product-spreader-settings/user-product/:userProductId +// @desc Get spreader settings for a specific user product +// @access Private +router.get('/user-product/:userProductId', validateParams(idParamSchema), async (req, res, next) => { + try { + const userProductId = req.params.userProductId; + + // Verify the user product belongs to the requesting user + const productCheck = await pool.query( + 'SELECT id FROM user_products WHERE id = $1 AND user_id = $2', + [userProductId, req.user.id] + ); + + if (productCheck.rows.length === 0) { + throw new AppError('User product not found', 404); + } + + const result = await pool.query( + `SELECT * FROM product_spreader_settings + WHERE user_product_id = $1 + ORDER BY spreader_brand, spreader_model NULLS LAST, setting_value`, + [userProductId] + ); + + res.json({ + success: true, + data: { + settings: result.rows.map(row => ({ + id: row.id, + spreaderBrand: row.spreader_brand, + spreaderModel: row.spreader_model, + settingValue: row.setting_value, + rateDescription: row.rate_description, + notes: row.notes, + createdAt: row.created_at + })) + } + }); + } catch (error) { + next(error); + } +}); + +// @route POST /api/product-spreader-settings +// @desc Add spreader setting to product +// @access Private +router.post('/', validateRequest(spreaderSettingSchema), async (req, res, next) => { + try { + const { productId, userProductId, spreaderBrand, spreaderModel, settingValue, rateDescription, notes } = req.body; + + // If it's a user product, verify ownership + if (userProductId) { + const productCheck = await pool.query( + 'SELECT id FROM user_products WHERE id = $1 AND user_id = $2', + [userProductId, req.user.id] + ); + + if (productCheck.rows.length === 0) { + throw new AppError('User product not found', 404); + } + } + + const result = await pool.query( + `INSERT INTO product_spreader_settings + (product_id, user_product_id, spreader_brand, spreader_model, setting_value, rate_description, notes) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *`, + [productId, userProductId, spreaderBrand, spreaderModel, settingValue, rateDescription, notes] + ); + + const setting = result.rows[0]; + + res.status(201).json({ + success: true, + message: 'Spreader setting added successfully', + data: { + setting: { + id: setting.id, + spreaderBrand: setting.spreader_brand, + spreaderModel: setting.spreader_model, + settingValue: setting.setting_value, + rateDescription: setting.rate_description, + notes: setting.notes, + createdAt: setting.created_at + } + } + }); + } catch (error) { + next(error); + } +}); + +// @route PUT /api/product-spreader-settings/:id +// @desc Update spreader setting +// @access Private +router.put('/:id', validateParams(idParamSchema), validateRequest(spreaderSettingSchema), async (req, res, next) => { + try { + const settingId = req.params.id; + const { productId, userProductId, spreaderBrand, spreaderModel, settingValue, rateDescription, notes } = req.body; + + // Check if setting exists and user has permission to edit it + let checkQuery; + let checkParams; + + if (userProductId) { + checkQuery = ` + SELECT pss.* FROM product_spreader_settings pss + JOIN user_products up ON pss.user_product_id = up.id + WHERE pss.id = $1 AND up.user_id = $2 + `; + checkParams = [settingId, req.user.id]; + } else { + // For shared products, any authenticated user can edit (you might want to restrict this) + checkQuery = 'SELECT * FROM product_spreader_settings WHERE id = $1'; + checkParams = [settingId]; + } + + const settingCheck = await pool.query(checkQuery, checkParams); + + if (settingCheck.rows.length === 0) { + throw new AppError('Spreader setting not found', 404); + } + + const result = await pool.query( + `UPDATE product_spreader_settings + SET spreader_brand = $1, spreader_model = $2, setting_value = $3, + rate_description = $4, notes = $5 + WHERE id = $6 + RETURNING *`, + [spreaderBrand, spreaderModel, settingValue, rateDescription, notes, settingId] + ); + + const setting = result.rows[0]; + + res.json({ + success: true, + message: 'Spreader setting updated successfully', + data: { + setting: { + id: setting.id, + spreaderBrand: setting.spreader_brand, + spreaderModel: setting.spreader_model, + settingValue: setting.setting_value, + rateDescription: setting.rate_description, + notes: setting.notes, + updatedAt: setting.updated_at + } + } + }); + } catch (error) { + next(error); + } +}); + +// @route DELETE /api/product-spreader-settings/:id +// @desc Delete spreader setting +// @access Private +router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => { + try { + const settingId = req.params.id; + + // Check if setting exists and user has permission to delete it + const settingCheck = await pool.query( + `SELECT pss.* FROM product_spreader_settings pss + LEFT JOIN user_products up ON pss.user_product_id = up.id + WHERE pss.id = $1 AND (pss.product_id IS NOT NULL OR up.user_id = $2)`, + [settingId, req.user.id] + ); + + if (settingCheck.rows.length === 0) { + throw new AppError('Spreader setting not found', 404); + } + + await pool.query('DELETE FROM product_spreader_settings WHERE id = $1', [settingId]); + + res.json({ + success: true, + message: 'Spreader setting deleted successfully' + }); + } catch (error) { + next(error); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/spreaderSettings.js b/backend/src/routes/spreaderSettings.js new file mode 100644 index 0000000..f268cbf --- /dev/null +++ b/backend/src/routes/spreaderSettings.js @@ -0,0 +1,93 @@ +const express = require('express'); +const pool = require('../config/database'); +const { AppError } = require('../middleware/errorHandler'); + +const router = express.Router(); + +// @route GET /api/spreader-settings +// @desc Get all spreader settings +// @access Private +router.get('/', async (req, res, next) => { + try { + const result = await pool.query( + `SELECT * FROM spreader_settings + ORDER BY spreader_brand, spreader_model NULLS LAST, setting_value` + ); + + // Group by brand for easier frontend consumption + const groupedSettings = result.rows.reduce((acc, setting) => { + const brand = setting.spreader_brand; + if (!acc[brand]) { + acc[brand] = []; + } + acc[brand].push({ + id: setting.id, + model: setting.spreader_model, + setting: setting.setting_value, + description: setting.application_rate_description + }); + return acc; + }, {}); + + res.json({ + success: true, + data: { + settings: result.rows, + groupedSettings + } + }); + } catch (error) { + next(error); + } +}); + +// @route GET /api/spreader-settings/brands +// @desc Get list of spreader brands +// @access Private +router.get('/brands', async (req, res, next) => { + try { + const result = await pool.query( + `SELECT DISTINCT spreader_brand as brand, + COUNT(*) as setting_count + FROM spreader_settings + GROUP BY spreader_brand + ORDER BY spreader_brand` + ); + + res.json({ + success: true, + data: { + brands: result.rows + } + }); + } catch (error) { + next(error); + } +}); + +// @route GET /api/spreader-settings/:brand +// @desc Get settings for a specific brand +// @access Private +router.get('/:brand', async (req, res, next) => { + try { + const brand = req.params.brand; + const result = await pool.query( + `SELECT * FROM spreader_settings + WHERE spreader_brand = $1 + ORDER BY spreader_model NULLS LAST, setting_value`, + [brand] + ); + + res.json({ + success: true, + data: { + brand, + settings: result.rows + } + }); + } catch (error) { + next(error); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/src/utils/applicationCalculations.js b/backend/src/utils/applicationCalculations.js index 51b4b8f..ea9c5eb 100644 --- a/backend/src/utils/applicationCalculations.js +++ b/backend/src/utils/applicationCalculations.js @@ -133,7 +133,8 @@ function calculateGranularApplication(areaSquareFeet, rateAmount, rateUnit, equi console.log(`Calculating granular application: Area: ${areaSquareFeet} sq ft (${areaAcres.toFixed(3)} acres, ${area1000sqft.toFixed(1)} x 1000sqft) Rate: ${rateAmount} ${rateUnit} - Equipment: ${equipment?.categoryName}`); + Equipment: ${equipment?.categoryName} (${equipment?.spreaderBrand || 'unknown brand'}) + Spreader Setting: ${equipment?.spreaderSetting || 'not specified'}`); // Calculate product amount based on rate unit if (rateUnit.includes('lbs/1000sqft') || rateUnit.includes('lbs per 1000sqft') || rateUnit.includes('lb/1000sqft')) { @@ -145,6 +146,17 @@ function calculateGranularApplication(areaSquareFeet, rateAmount, rateUnit, equi } else if (rateUnit.includes('oz/1000sqft') || rateUnit.includes('oz per 1000sqft')) { // Rate is ounces per 1000 square feet, convert to pounds productPounds = (area1000sqft * rateAmount) / 16; // 16 oz = 1 lb + } else if (rateUnit.includes('covers') || rateUnit.includes('coverage')) { + // Handle bag coverage rates like "50 lb covers 16,000 sq ft" + // Format: "weight coverage_area" (e.g., "50 16000") + // rateAmount should be the coverage area per unit weight + const coveragePerPound = rateAmount; // sq ft per pound + productPounds = areaSquareFeet / coveragePerPound; + console.log(`Bag coverage calculation: ${areaSquareFeet} sq ft / ${coveragePerPound} sq ft per lb = ${productPounds.toFixed(2)} lbs`); + } else if (rateUnit.includes('lb covers') || rateUnit.includes('lbs cover')) { + // Alternative format: "1 lb covers X sq ft" + const coveragePerPound = rateAmount; + productPounds = areaSquareFeet / coveragePerPound; } else { // Fallback: assume rate is per 1000 sq ft productPounds = area1000sqft * rateAmount; diff --git a/database/migrations/add_spreader_settings.sql b/database/migrations/add_spreader_settings.sql new file mode 100644 index 0000000..91dbcaa --- /dev/null +++ b/database/migrations/add_spreader_settings.sql @@ -0,0 +1,31 @@ +-- Add spreader settings for granular applications +-- This allows storing different spreader settings for various equipment brands + +CREATE TABLE IF NOT EXISTS spreader_settings ( + id SERIAL PRIMARY KEY, + spreader_brand VARCHAR(100) NOT NULL, + spreader_model VARCHAR(100), + setting_value VARCHAR(10) NOT NULL, + application_rate_description VARCHAR(200), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Add some common spreader settings from the LESCO example +INSERT INTO spreader_settings (spreader_brand, spreader_model, setting_value, application_rate_description) VALUES +('LESCO', 'All Rotary Models', '#14', '1 lb nitrogen per 1000 sq ft'), +('PermaGreen', NULL, '14', '1 lb nitrogen per 1000 sq ft'), +('Cyclone', NULL, '4', '1 lb nitrogen per 1000 sq ft'), +('Spyker', NULL, '4', '1 lb nitrogen per 1000 sq ft'), +('Z-Spray', NULL, '4', '1 lb nitrogen per 1000 sq ft'), +('Vicon', 'LESCO Pendulum', '20', '1 lb nitrogen per 1000 sq ft'); + +-- Add spreader setting field to user equipment for spreaders +ALTER TABLE user_equipment +ADD COLUMN IF NOT EXISTS spreader_setting VARCHAR(10), +ADD COLUMN IF NOT EXISTS spreader_brand VARCHAR(100); + +-- Create index for better performance +CREATE INDEX IF NOT EXISTS idx_spreader_settings_brand ON spreader_settings(spreader_brand); +CREATE INDEX IF NOT EXISTS idx_user_equipment_spreader ON user_equipment(spreader_brand); + +SELECT 'Added spreader settings support successfully!' as migration_status; \ No newline at end of file diff --git a/database/migrations/user_spreader_system.sql b/database/migrations/user_spreader_system.sql new file mode 100644 index 0000000..7d9ad9b --- /dev/null +++ b/database/migrations/user_spreader_system.sql @@ -0,0 +1,56 @@ +-- Update user equipment for better spreader support +-- Note: we already have 'manufacturer' and 'model' columns, so let's use those +-- Add brand as alias for manufacturer for consistency with new API +ALTER TABLE user_equipment +ADD COLUMN IF NOT EXISTS brand VARCHAR(100), +ADD COLUMN IF NOT EXISTS notes TEXT; + +-- Copy existing manufacturer data to brand field for consistency +UPDATE user_equipment SET brand = manufacturer WHERE brand IS NULL AND manufacturer IS NOT NULL; + +-- Create table to store product-specific spreader settings +-- This links products to specific spreader settings +CREATE TABLE IF NOT EXISTS product_spreader_settings ( + id SERIAL PRIMARY KEY, + product_id INTEGER REFERENCES products(id) ON DELETE CASCADE, + user_product_id INTEGER REFERENCES user_products(id) ON DELETE CASCADE, + spreader_brand VARCHAR(100) NOT NULL, + spreader_model VARCHAR(100), + setting_value VARCHAR(20) NOT NULL, + rate_description VARCHAR(200), -- e.g., "1 lb nitrogen per 1000 sq ft" + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- Ensure we have either product_id or user_product_id, but not both + CHECK ( + (product_id IS NOT NULL AND user_product_id IS NULL) OR + (product_id IS NULL AND user_product_id IS NOT NULL) + ) +); + +-- Create indexes for better performance +CREATE INDEX IF NOT EXISTS idx_product_spreader_settings_product ON product_spreader_settings(product_id); +CREATE INDEX IF NOT EXISTS idx_product_spreader_settings_user_product ON product_spreader_settings(user_product_id); +CREATE INDEX IF NOT EXISTS idx_product_spreader_settings_brand ON product_spreader_settings(spreader_brand); +CREATE INDEX IF NOT EXISTS idx_user_equipment_brand_model ON user_equipment(brand, model); + +-- Add trigger to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_product_spreader_settings_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +DROP TRIGGER IF EXISTS update_product_spreader_settings_updated_at_trigger ON product_spreader_settings; +CREATE TRIGGER update_product_spreader_settings_updated_at_trigger + BEFORE UPDATE ON product_spreader_settings + FOR EACH ROW + EXECUTE PROCEDURE update_product_spreader_settings_updated_at(); + +-- Drop the old spreader_settings table since we're using a different approach +DROP TABLE IF EXISTS spreader_settings; + +SELECT 'Updated spreader system for user-defined spreaders and product settings!' as migration_status; \ No newline at end of file diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index 3f52cfe..4b4842e 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -275,7 +275,7 @@ const Applications = () => { const planPayload = { lawnSectionId: parseInt(planData.selectedAreas[0]), equipmentId: parseInt(planData.equipmentId), - nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null, + ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], notes: planData.notes || '', areaSquareFeet: areaSquareFeet, @@ -288,11 +288,13 @@ const Applications = () => { capacityLbs: selectedEquipment?.capacityLbs, spreadWidth: selectedEquipment?.spreadWidth }, - nozzle: selectedNozzle ? { - id: selectedNozzle.id, - flowRateGpm: selectedNozzle.flowRateGpm, - sprayAngle: selectedNozzle.sprayAngle - } : null, + ...(planData.applicationType === 'liquid' && selectedNozzle && { + nozzle: { + id: selectedNozzle.id, + flowRateGpm: selectedNozzle.flowRateGpm, + sprayAngle: selectedNozzle.sprayAngle + } + }), products: [{ ...(planData.selectedProduct?.isShared ? { productId: parseInt(planData.selectedProduct.id) } @@ -317,7 +319,7 @@ const Applications = () => { const planPayload = { lawnSectionId: parseInt(areaId), equipmentId: parseInt(planData.equipmentId), - nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null, + ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: new Date().toISOString().split('T')[0], notes: planData.notes || '', areaSquareFeet: areaSquareFeet, @@ -330,11 +332,13 @@ const Applications = () => { capacityLbs: selectedEquipment?.capacityLbs, spreadWidth: selectedEquipment?.spreadWidth }, - nozzle: selectedNozzle ? { - id: selectedNozzle.id, - flowRateGpm: selectedNozzle.flowRateGpm, - sprayAngle: selectedNozzle.sprayAngle - } : null, + ...(planData.applicationType === 'liquid' && selectedNozzle && { + nozzle: { + id: selectedNozzle.id, + flowRateGpm: selectedNozzle.flowRateGpm, + sprayAngle: selectedNozzle.sprayAngle + } + }), products: [{ ...(planData.selectedProduct?.isShared ? { productId: parseInt(planData.selectedProduct.id) } diff --git a/frontend/src/pages/Equipment/Equipment.js b/frontend/src/pages/Equipment/Equipment.js index ecac7f1..5bcf957 100644 --- a/frontend/src/pages/Equipment/Equipment.js +++ b/frontend/src/pages/Equipment/Equipment.js @@ -493,6 +493,7 @@ const EquipmentFormModal = ({ isEdit, equipment, categories, equipmentTypes, onS customName: equipment?.customName || '', manufacturer: equipment?.manufacturer || '', model: equipment?.model || '', + notes: equipment?.notes || '', // Spreader fields capacityLbs: equipment?.capacityLbs || '', spreaderType: equipment?.spreaderType || 'walk_behind', diff --git a/frontend/src/pages/Products/Products.js b/frontend/src/pages/Products/Products.js index 936f901..62f4219 100644 --- a/frontend/src/pages/Products/Products.js +++ b/frontend/src/pages/Products/Products.js @@ -7,7 +7,7 @@ import { TrashIcon, PencilIcon } from '@heroicons/react/24/outline'; -import { productsAPI } from '../../services/api'; +import { productsAPI, productSpreaderSettingsAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import toast from 'react-hot-toast'; @@ -67,8 +67,27 @@ const Products = () => { const handleCreateProduct = async (productData) => { try { - await productsAPI.createUserProduct(productData); - toast.success('Custom product created successfully!'); + // Create the product first + const response = await productsAPI.createUserProduct(productData); + const createdProduct = response.data.data.product; + + // Save spreader settings if any + if (productData.spreaderSettings && productData.spreaderSettings.length > 0) { + const settingPromises = productData.spreaderSettings.map(setting => + productSpreaderSettingsAPI.create({ + userProductId: createdProduct.id, + spreaderBrand: setting.spreaderBrand, + spreaderModel: setting.spreaderModel || null, + settingValue: setting.settingValue, + rateDescription: setting.rateDescription || null, + notes: setting.notes || null + }) + ); + + 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) { @@ -397,6 +416,35 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories }) notes: '' }); + const [spreaderSettings, setSpreaderSettings] = useState([]); + const [newSpreaderSetting, setNewSpreaderSetting] = useState({ + spreaderBrand: '', + spreaderModel: '', + settingValue: '', + rateDescription: '', + notes: '' + }); + + const addSpreaderSetting = () => { + if (!newSpreaderSetting.spreaderBrand || !newSpreaderSetting.settingValue) { + toast.error('Please enter spreader brand and setting value'); + return; + } + + setSpreaderSettings([...spreaderSettings, { ...newSpreaderSetting, id: Date.now() }]); + setNewSpreaderSetting({ + spreaderBrand: '', + spreaderModel: '', + settingValue: '', + rateDescription: '', + notes: '' + }); + }; + + const removeSpreaderSetting = (id) => { + setSpreaderSettings(spreaderSettings.filter(setting => setting.id !== id)); + }; + const handleSubmit = (e) => { e.preventDefault(); @@ -416,7 +464,8 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories }) customName: formData.customName || null, customRateAmount: formData.customRateAmount ? parseFloat(formData.customRateAmount) : null, customRateUnit: formData.customRateUnit || null, - notes: formData.notes || null + notes: formData.notes || null, + spreaderSettings: formData.productType === 'granular' ? spreaderSettings : [] }; onSubmit(submitData); @@ -547,6 +596,98 @@ const CreateProductModal = ({ onSubmit, onCancel, sharedProducts, categories }) + {/* 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.spreaderBrand} {setting.spreaderModel && `${setting.spreaderModel}`} - Setting: {setting.settingValue} +
+ {setting.rateDescription && ( +
{setting.rateDescription}
+ )} +
+ +
+ ))} +
+ )} + + {/* Add new spreader setting form */} +
+
Add Spreader Setting
+
+
+ + setNewSpreaderSetting({ ...newSpreaderSetting, spreaderBrand: e.target.value })} + placeholder="LESCO, PermaGreen, Cyclone, etc." + /> +
+
+ + setNewSpreaderSetting({ ...newSpreaderSetting, spreaderModel: e.target.value })} + placeholder="Model name/number" + /> +
+
+
+
+ + 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" + /> +
+
+ +
+
+ )} +