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 && (