diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index f636292..1061f53 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -7,6 +7,37 @@ const { calculateApplication } = require('../utils/applicationCalculations'); const router = express.Router(); +// Helper function to get spreader settings for equipment and product +async function getSpreaderSettingsForEquipment(equipmentId, productId, userProductId, userId) { + // Get spreader settings for the specific product and equipment combination + let query, params; + + if (userProductId) { + query = ` + SELECT pss.*, ue.custom_name as equipment_name, ue.manufacturer, ue.model as equipment_model + FROM product_spreader_settings pss + JOIN user_equipment ue ON pss.equipment_id = ue.id + WHERE pss.user_product_id = $1 AND pss.equipment_id = $2 AND ue.user_id = $3 + ORDER BY pss.created_at DESC + LIMIT 1 + `; + params = [userProductId, equipmentId, userId]; + } else { + query = ` + SELECT pss.*, ue.custom_name as equipment_name, ue.manufacturer, ue.model as equipment_model + FROM product_spreader_settings pss + JOIN user_equipment ue ON pss.equipment_id = ue.id + WHERE pss.product_id = $1 AND pss.equipment_id = $2 AND ue.user_id = $3 + ORDER BY pss.created_at DESC + LIMIT 1 + `; + params = [productId, equipmentId, userId]; + } + + const result = await pool.query(query, params); + return result.rows[0] || null; +} + // @route GET /api/applications/plans // @desc Get all application plans for current user // @access Private @@ -902,4 +933,67 @@ router.get('/stats', async (req, res, next) => { } }); +// @route GET /api/applications/spreader-settings/:equipmentId/:productId +// @desc Get recommended spreader settings for specific equipment and product combination +// @access Private +router.get('/spreader-settings/:equipmentId/:productId', async (req, res, next) => { + try { + const { equipmentId, productId } = req.params; + const { isUserProduct } = req.query; // Indicates if productId refers to user_products table + + // Verify equipment belongs to user + const equipmentCheck = await pool.query( + 'SELECT id, custom_name, manufacturer, model FROM user_equipment WHERE id = $1 AND user_id = $2', + [equipmentId, req.user.id] + ); + + if (equipmentCheck.rows.length === 0) { + throw new AppError('Equipment not found', 404); + } + + const equipment = equipmentCheck.rows[0]; + + // Get spreader settings + const spreaderSetting = await getSpreaderSettingsForEquipment( + parseInt(equipmentId), + isUserProduct === 'true' ? null : parseInt(productId), + isUserProduct === 'true' ? parseInt(productId) : null, + req.user.id + ); + + if (!spreaderSetting) { + return res.json({ + success: true, + data: { + hasSettings: false, + message: 'No spreader settings found for this equipment and product combination', + equipment: { + id: equipment.id, + name: equipment.custom_name || `${equipment.manufacturer} ${equipment.model}`.trim() + } + } + }); + } + + res.json({ + success: true, + data: { + hasSettings: true, + setting: { + id: spreaderSetting.id, + settingValue: spreaderSetting.setting_value, + rateDescription: spreaderSetting.rate_description, + notes: spreaderSetting.notes + }, + equipment: { + id: equipment.id, + name: equipment.custom_name || `${equipment.manufacturer} ${equipment.model}`.trim() + } + } + }); + } catch (error) { + next(error); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/src/routes/equipment.js b/backend/src/routes/equipment.js index b10f8a5..885cd0d 100644 --- a/backend/src/routes/equipment.js +++ b/backend/src/routes/equipment.js @@ -182,6 +182,43 @@ router.get('/', async (req, res, next) => { } }); +// @route GET /api/equipment/spreaders +// @desc Get all spreader equipment for current user (for product spreader settings) +// @access Private +router.get('/spreaders', async (req, res, next) => { + try { + const result = await pool.query( + `SELECT ue.id, ue.custom_name, ue.manufacturer, ue.model, ue.spreader_type, + ue.capacity_lbs, ue.spread_width, ec.name as category_name + FROM user_equipment ue + LEFT JOIN equipment_categories ec ON ue.category_id = ec.id + WHERE ue.user_id = $1 + AND ue.is_active = true + AND (ec.name ILIKE '%spreader%' OR ue.spreader_type IS NOT NULL) + ORDER BY ue.custom_name, ue.manufacturer, ue.model`, + [req.user.id] + ); + + res.json({ + success: true, + data: { + spreaders: result.rows.map(item => ({ + id: item.id, + name: item.custom_name || `${item.manufacturer} ${item.model}`.trim(), + manufacturer: item.manufacturer, + model: item.model, + spreaderType: item.spreader_type, + capacityLbs: item.capacity_lbs ? parseFloat(item.capacity_lbs) : null, + spreadWidth: item.spread_width ? parseFloat(item.spread_width) : null, + categoryName: item.category_name + })) + } + }); + } catch (error) { + next(error); + } +}); + // @route GET /api/equipment/:id // @desc Get single equipment item // @access Private diff --git a/backend/src/routes/products.js b/backend/src/routes/products.js index 91f972f..7209ad1 100644 --- a/backend/src/routes/products.js +++ b/backend/src/routes/products.js @@ -351,13 +351,34 @@ router.post('/user', validateRequest(userProductSchema), async (req, res, next) if (spreaderSettings && Array.isArray(spreaderSettings) && productType === 'granular') { // Add spreader settings for this user product 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)`, - [userProduct.id, setting.spreaderBrand, setting.spreaderModel, setting.settingValue, - setting.rateDescription, setting.notes] - ); + // Check if equipment exists and belongs to user if equipmentId is provided + if (setting.equipmentId) { + const equipmentCheck = await pool.query( + 'SELECT id FROM user_equipment WHERE id = $1 AND user_id = $2', + [setting.equipmentId, req.user.id] + ); + + if (equipmentCheck.rows.length === 0) { + throw new AppError(`Equipment with id ${setting.equipmentId} not found`, 404); + } + + await pool.query( + `INSERT INTO product_spreader_settings + (user_product_id, equipment_id, setting_value, rate_description, notes) + VALUES ($1, $2, $3, $4, $5)`, + [userProduct.id, setting.equipmentId, setting.settingValue, + setting.rateDescription, setting.notes] + ); + } else { + // Fall back to legacy brand/model approach + 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)`, + [userProduct.id, setting.spreaderBrand, setting.spreaderModel, setting.settingValue, + setting.rateDescription, setting.notes] + ); + } } } @@ -412,17 +433,27 @@ router.get('/user/:id', validateParams(idParamSchema), async (req, res, next) => const userProduct = result.rows[0]; - // Get spreader settings for this user product + // Get spreader settings for this user product with equipment details let spreaderSettings = []; const settingsResult = await pool.query( - `SELECT * FROM product_spreader_settings - WHERE user_product_id = $1 - ORDER BY spreader_brand, spreader_model NULLS LAST, setting_value`, + `SELECT pss.*, ue.custom_name as equipment_name, ue.manufacturer, ue.model as equipment_model, + ue.spreader_type, ue.capacity_lbs + FROM product_spreader_settings pss + LEFT JOIN user_equipment ue ON pss.equipment_id = ue.id + WHERE pss.user_product_id = $1 + ORDER BY ue.custom_name NULLS LAST, pss.spreader_brand, pss.spreader_model NULLS LAST, pss.setting_value`, [userProductId] ); spreaderSettings = settingsResult.rows.map(row => ({ id: row.id, + equipmentId: row.equipment_id, + equipmentName: row.equipment_name, + equipmentManufacturer: row.manufacturer, + equipmentModel: row.equipment_model, + equipmentType: row.spreader_type, + equipmentCapacity: row.capacity_lbs ? parseFloat(row.capacity_lbs) : null, + // Legacy fields spreaderBrand: row.spreader_brand, spreaderModel: row.spreader_model, settingValue: row.setting_value, diff --git a/backend/src/utils/validation.js b/backend/src/utils/validation.js index 6d0c35b..c5f4d00 100644 --- a/backend/src/utils/validation.js +++ b/backend/src/utils/validation.js @@ -91,8 +91,10 @@ const userProductSchema = Joi.object({ // Spreader settings for granular products spreaderSettings: Joi.array().items( Joi.object({ - id: Joi.number().optional(), // For frontend temporary IDs - spreaderBrand: Joi.string().max(100).required(), + id: Joi.number().optional(), // For existing settings + equipmentId: Joi.number().integer().positive().optional(), // Link to user_equipment + // Legacy fields for backward compatibility + spreaderBrand: Joi.string().max(100).optional(), spreaderModel: Joi.alternatives().try( Joi.string().max(100).allow(''), Joi.allow(null) @@ -106,7 +108,7 @@ const userProductSchema = Joi.object({ Joi.string().allow(''), Joi.allow(null) ).optional() - }) + }).or('equipmentId', 'spreaderBrand') // Must have either equipment reference or brand ).optional() }); diff --git a/database/migrations/link_spreader_settings_to_equipment.sql b/database/migrations/link_spreader_settings_to_equipment.sql new file mode 100644 index 0000000..e3ff8c5 --- /dev/null +++ b/database/migrations/link_spreader_settings_to_equipment.sql @@ -0,0 +1,14 @@ +-- Link spreader settings to actual user equipment instead of storing text +-- This allows better integration and recommendations + +-- Add equipment reference to product_spreader_settings +ALTER TABLE product_spreader_settings +ADD COLUMN IF NOT EXISTS equipment_id INTEGER REFERENCES user_equipment(id) ON DELETE CASCADE; + +-- Create index for the new relationship +CREATE INDEX IF NOT EXISTS idx_product_spreader_settings_equipment ON product_spreader_settings(equipment_id); + +-- We'll keep the old columns for backward compatibility during transition +-- They can be removed later once all data is migrated + +SELECT 'Added equipment reference to spreader settings!' as migration_status; \ No newline at end of file