spreader stuff
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
241
backend/src/routes/productSpreaderSettings.js
Normal file
241
backend/src/routes/productSpreaderSettings.js
Normal file
@@ -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;
|
||||
93
backend/src/routes/spreaderSettings.js
Normal file
93
backend/src/routes/spreaderSettings.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user