From 054f743e2d61b1b9cd3360642cc988ca3b802e55 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Tue, 26 Aug 2025 06:58:21 -0500 Subject: [PATCH] tank mix 1 --- backend/src/routes/applications.js | 76 ++-- .../src/pages/Applications/Applications.js | 326 ++++++++++++++---- 2 files changed, 306 insertions(+), 96 deletions(-) diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index d2e5239..07672eb 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -89,44 +89,59 @@ router.get('/plans', async (req, res, next) => { queryParams ); - // Get spreader settings for each plan + // Get spreader settings and product details for each plan const plansWithSettings = await Promise.all( result.rows.map(async (plan) => { let spreaderSetting = null; + let productDetails = []; + // Get all products for this plan + const productsResult = await pool.query( + `SELECT app.*, + COALESCE(p.name, up.custom_name) as product_name, + COALESCE(p.brand, up.custom_brand) as product_brand, + COALESCE(p.product_type, up.custom_product_type) as product_type, + p.name as shared_name, + up.custom_name as user_name + FROM application_plan_products app + LEFT JOIN products p ON app.product_id = p.id + LEFT JOIN user_products up ON app.user_product_id = up.id + WHERE app.plan_id = $1`, + [plan.id] + ); + + productDetails = productsResult.rows.map(product => ({ + name: product.product_name, + brand: product.product_brand, + type: product.product_type, + rateAmount: parseFloat(product.rate_amount || 0), + rateUnit: product.rate_unit, + calculatedAmount: parseFloat(product.calculated_product_amount || 0), + isShared: !!product.shared_name + })); + // Only get spreader settings for granular applications with equipment - if (plan.equipment_name) { - // Get the first product for this plan to determine if it's granular - const productResult = await pool.query( - `SELECT app.product_id, app.user_product_id, p.product_type as shared_type, up.custom_product_type as user_type - FROM application_plan_products app - LEFT JOIN products p ON app.product_id = p.id - LEFT JOIN user_products up ON app.user_product_id = up.id - WHERE app.plan_id = $1 - LIMIT 1`, - [plan.id] - ); + if (plan.equipment_name && productsResult.rows.length > 0) { + const firstProduct = productsResult.rows[0]; + const productType = firstProduct.shared_name ? + (await pool.query('SELECT product_type FROM products WHERE id = $1', [firstProduct.product_id])).rows[0]?.product_type : + firstProduct.custom_product_type; - if (productResult.rows.length > 0) { - const product = productResult.rows[0]; - const productType = product.shared_type || product.user_type; + if (productType === 'granular') { + // Get equipment ID + const equipmentResult = await pool.query( + 'SELECT id FROM user_equipment WHERE custom_name = $1 AND user_id = $2', + [plan.equipment_name, req.user.id] + ); - if (productType === 'granular') { - // Get equipment ID - const equipmentResult = await pool.query( - 'SELECT id FROM user_equipment WHERE custom_name = $1 AND user_id = $2', - [plan.equipment_name, req.user.id] + if (equipmentResult.rows.length > 0) { + const equipmentId = equipmentResult.rows[0].id; + spreaderSetting = await getSpreaderSettingsForEquipment( + equipmentId, + firstProduct.product_id, + firstProduct.user_product_id, + req.user.id ); - - if (equipmentResult.rows.length > 0) { - const equipmentId = equipmentResult.rows[0].id; - spreaderSetting = await getSpreaderSettingsForEquipment( - equipmentId, - product.product_id, - product.user_product_id, - req.user.id - ); - } } } } @@ -146,6 +161,7 @@ router.get('/plans', async (req, res, next) => { totalWaterAmount: parseFloat(plan.total_water_amount || 0), avgSpeedMph: parseFloat(plan.avg_speed_mph || 0), spreaderSetting: spreaderSetting?.setting_value || null, + productDetails: productDetails, createdAt: plan.created_at, updatedAt: plan.updated_at }; diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index d2ab244..f382fb8 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -301,18 +301,38 @@ const Applications = () => { Products: {application.productCount}

{/* Display calculated amounts */} - {application.totalProductAmount > 0 && ( + {(application.totalProductAmount > 0 || (application.productDetails && application.productDetails.length > 0)) && (

Calculated Requirements:

-

• Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}

- {application.totalWaterAmount > 0 && ( -

• Water: {application.totalWaterAmount.toFixed(2)} gallons

- )} - {application.avgSpeedMph > 0 && ( -

• Target Speed: {application.avgSpeedMph.toFixed(1)} mph

- )} - {application.spreaderSetting && ( -

• Spreader Setting: {application.spreaderSetting}

+ + {/* Show individual products for liquid tank mix */} + {application.productDetails && application.productDetails.length > 1 ? ( + <> + {application.productDetails.map((product, index) => ( +

+ • {product.name}{product.brand ? ` (${product.brand})` : ''}: {product.calculatedAmount.toFixed(2)} oz +

+ ))} + {application.totalWaterAmount > 0 && ( +

• Water: {application.totalWaterAmount.toFixed(2)} gallons

+ )} + {application.avgSpeedMph > 0 && ( +

• Target Speed: {application.avgSpeedMph.toFixed(1)} mph

+ )} + + ) : ( + <> +

• Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}

+ {application.totalWaterAmount > 0 && ( +

• Water: {application.totalWaterAmount.toFixed(2)} gallons

+ )} + {application.avgSpeedMph > 0 && ( +

• Target Speed: {application.avgSpeedMph.toFixed(1)} mph

+ )} + {application.spreaderSetting && ( +

• Spreader Setting: {application.spreaderSetting}

+ )} + )}
)} @@ -398,15 +418,25 @@ const Applications = () => { sprayAngle: selectedNozzle.sprayAngle } }), - products: [{ - ...(planData.selectedProduct?.isShared - ? { productId: parseInt(planData.selectedProduct.id) } - : { userProductId: parseInt(planData.selectedProduct.id) } - ), - rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1), - rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft', - applicationType: planData.applicationType - }] + products: planData.applicationType === 'liquid' + ? planData.selectedProducts.map(item => ({ + ...(item.product?.isShared + ? { productId: parseInt(item.product.id) } + : { userProductId: parseInt(item.product.id) } + ), + rateAmount: parseFloat(item.rateAmount || 1), + rateUnit: item.rateUnit || 'oz/1000 sq ft', + applicationType: planData.applicationType + })) + : [{ + ...(planData.selectedProduct?.isShared + ? { productId: parseInt(planData.selectedProduct.id) } + : { userProductId: parseInt(planData.selectedProduct.id) } + ), + rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1), + rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft', + applicationType: planData.applicationType + }] }; await applicationsAPI.updatePlan(editingPlan.id, planPayload); @@ -442,15 +472,25 @@ const Applications = () => { sprayAngle: selectedNozzle.sprayAngle } }), - products: [{ - ...(planData.selectedProduct?.isShared - ? { productId: parseInt(planData.selectedProduct.id) } - : { userProductId: parseInt(planData.selectedProduct.id) } - ), - rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1), - rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft', - applicationType: planData.applicationType - }] + products: planData.applicationType === 'liquid' + ? planData.selectedProducts.map(item => ({ + ...(item.product?.isShared + ? { productId: parseInt(item.product.id) } + : { userProductId: parseInt(item.product.id) } + ), + rateAmount: parseFloat(item.rateAmount || 1), + rateUnit: item.rateUnit || 'oz/1000 sq ft', + applicationType: planData.applicationType + })) + : [{ + ...(planData.selectedProduct?.isShared + ? { productId: parseInt(planData.selectedProduct.id) } + : { userProductId: parseInt(planData.selectedProduct.id) } + ), + rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1), + rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft', + applicationType: planData.applicationType + }] }; return applicationsAPI.createPlan(planPayload); @@ -493,6 +533,7 @@ const ApplicationPlanModal = ({ selectedAreas: [], productId: '', selectedProduct: null, + selectedProducts: [], // For liquid tank mixing - array of {product, rate} applicationType: '', // 'liquid' or 'granular' equipmentId: '', nozzleId: '', @@ -564,11 +605,30 @@ const ApplicationPlanModal = ({ return; } - if (!planData.productId) { - toast.error('Please select a product'); + if (!planData.applicationType) { + toast.error('Please select an application type'); return; } + // Validate product selection based on application type + if (planData.applicationType === 'granular') { + if (!planData.productId) { + toast.error('Please select a product'); + return; + } + } else if (planData.applicationType === 'liquid') { + if (planData.selectedProducts.length === 0) { + toast.error('Please select at least one product for tank mixing'); + return; + } + // Validate that all selected products have rates + const missingRates = planData.selectedProducts.filter(p => !p.rateAmount || p.rateAmount <= 0); + if (missingRates.length > 0) { + toast.error('Please enter application rates for all selected products'); + return; + } + } + if (!planData.equipmentId) { toast.error('Please select equipment'); return; @@ -686,58 +746,192 @@ const ApplicationPlanModal = ({ )} - {/* Product Selection */} + {/* Application Type Selection */}
+ {/* Product Selection - Single for Granular */} + {planData.applicationType === 'granular' && ( +
+ + +
+ )} + + {/* Product Selection - Multiple for Liquid Tank Mix */} + {planData.applicationType === 'liquid' && ( +
+ + + {/* Selected Products List */} + {planData.selectedProducts.length > 0 && ( +
+ {planData.selectedProducts.map((item, index) => ( +
+
+ {item.product.customName || item.product.name} + {item.product.brand || item.product.customBrand ? ( + - {item.product.brand || item.product.customBrand} + ) : null} +
+
+ { + const newProducts = [...planData.selectedProducts]; + newProducts[index] = { + ...item, + rateAmount: parseFloat(e.target.value) || 0 + }; + setPlanData({ ...planData, selectedProducts: newProducts }); + }} + className="w-20 px-2 py-1 text-sm border rounded" + placeholder="Rate" + /> + + +
+
+ ))} +
+ )} + + {/* Add Product Dropdown */} + + + {planData.selectedProducts.length === 0 && ( +

+ Select liquid products to mix in the tank. You can add herbicides, surfactants, and other liquid products. +

+ )} +
+ )} + {/* Equipment Selection */} {planData.applicationType && (