diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index 07672eb..d482429 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -355,28 +355,53 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n const plan = planResult.rows[0]; + // Calculate shared water amount and speed for liquid applications + const sectionArea = areaSquareFeet || parseFloat(section.area); + const firstProduct = products[0]; + const isLiquid = firstProduct.applicationType === 'liquid'; + + let sharedWaterAmount = 0; + let sharedTargetSpeed = 3; + + if (isLiquid) { + // Prepare equipment and nozzle objects for water calculation + const equipmentForCalc = { + categoryName: equipmentData.category_name, + tankSizeGallons: equipmentData.tank_size_gallons, + sprayWidthFeet: equipmentData.spray_width_feet, + capacityLbs: equipmentData.capacity_lbs, + spreadWidth: equipmentData.spread_width + }; + + const nozzleForCalc = nozzleData ? { + flowRateGpm: nozzleData.flow_rate_gpm, + sprayAngle: nozzleData.spray_angle + } : null; + + // Calculate water and speed once for the entire application + const waterCalculation = calculateApplication({ + areaSquareFeet: sectionArea, + rateAmount: 1, // Use dummy rate for water calculation + rateUnit: 'oz/1000 sq ft', + applicationType: 'liquid', + equipment: equipmentForCalc, + nozzle: nozzleForCalc + }); + + sharedWaterAmount = waterCalculation.waterAmountGallons || 0; + sharedTargetSpeed = waterCalculation.applicationSpeedMph || 3; + + console.log('Shared liquid calculation:', { + sectionArea, + sharedWaterAmount, + sharedTargetSpeed, + productsCount: products.length + }); + } + // Add products to plan with calculations for (const product of products) { const { productId, userProductId, rateAmount, rateUnit, applicationType } = product; - - // Use passed area or get from database - const sectionArea = areaSquareFeet || parseFloat(section.area); - - console.log('Calculation inputs:', { - areaSquareFeet: sectionArea, - rateAmount: parseFloat(rateAmount), - rateUnit, - applicationType, - equipmentData: { - category_name: equipmentData.category_name, - spray_width_feet: equipmentData.spray_width_feet, - tank_size_gallons: equipmentData.tank_size_gallons - }, - nozzleData: nozzleData ? { - flow_rate_gpm: nozzleData.flow_rate_gpm, - spray_angle: nozzleData.spray_angle - } : null - }); // Prepare equipment object for calculations const equipmentForCalc = { @@ -393,7 +418,7 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n sprayAngle: nozzleData.spray_angle } : null; - // Perform advanced calculations using the calculation engine + // Calculate product amount const calculations = calculateApplication({ areaSquareFeet: sectionArea, rateAmount: parseFloat(rateAmount), @@ -403,7 +428,12 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n nozzle: nozzleForCalc }); - console.log('Plan creation calculations:', calculations); + console.log('Individual product calculation:', { + product: productId || userProductId, + rateAmount, + rateUnit, + calculatedAmount: calculations.productAmountOunces || calculations.productAmountPounds + }); // Extract calculated values based on application type let calculatedProductAmount = 0; @@ -412,7 +442,9 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n if (calculations.type === 'liquid') { calculatedProductAmount = calculations.productAmountOunces || 0; - calculatedWaterAmount = calculations.waterAmountGallons || 0; + // Use shared water amount for liquid applications + calculatedWaterAmount = sharedWaterAmount; + targetSpeed = sharedTargetSpeed; } else if (calculations.type === 'granular') { calculatedProductAmount = calculations.productAmountPounds || 0; calculatedWaterAmount = 0; // No water for granular @@ -565,11 +597,46 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat // Delete existing products await client.query('DELETE FROM application_plan_products WHERE plan_id = $1', [planId]); + // Calculate shared water amount and speed for liquid applications + const sectionArea = areaSquareFeet || parseFloat(section.area); + const firstProduct = products[0]; + const isLiquid = firstProduct.applicationType === 'liquid'; + + let sharedWaterAmount = 0; + let sharedTargetSpeed = 3; + + if (isLiquid) { + // Prepare equipment and nozzle objects for water calculation + const equipmentForCalc = { + categoryName: equipmentData.category_name, + tankSizeGallons: equipmentData.tank_size_gallons, + sprayWidthFeet: equipmentData.spray_width_feet, + capacityLbs: equipmentData.capacity_lbs, + spreadWidth: equipmentData.spread_width + }; + + const nozzleForCalc = nozzleData ? { + flowRateGpm: nozzleData.flow_rate_gpm, + sprayAngle: nozzleData.spray_angle + } : null; + + // Calculate water and speed once for the entire application + const waterCalculation = calculateApplication({ + areaSquareFeet: sectionArea, + rateAmount: 1, // Use dummy rate for water calculation + rateUnit: 'oz/1000 sq ft', + applicationType: 'liquid', + equipment: equipmentForCalc, + nozzle: nozzleForCalc + }); + + sharedWaterAmount = waterCalculation.waterAmountGallons || 0; + sharedTargetSpeed = waterCalculation.applicationSpeedMph || 3; + } + // Add updated products with recalculation for (const product of products) { const { productId, userProductId, rateAmount, rateUnit, applicationType } = product; - - const sectionArea = areaSquareFeet || parseFloat(section.area); // Prepare equipment object for calculations const equipmentForCalc = { @@ -603,7 +670,9 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat if (calculations.type === 'liquid') { calculatedProductAmount = calculations.productAmountOunces || 0; - calculatedWaterAmount = calculations.waterAmountGallons || 0; + // Use shared water amount for liquid applications + calculatedWaterAmount = sharedWaterAmount; + targetSpeed = sharedTargetSpeed; } else if (calculations.type === 'granular') { calculatedProductAmount = calculations.productAmountPounds || 0; calculatedWaterAmount = 0; diff --git a/backend/src/utils/applicationCalculations.js b/backend/src/utils/applicationCalculations.js index ea9c5eb..5b8950a 100644 --- a/backend/src/utils/applicationCalculations.js +++ b/backend/src/utils/applicationCalculations.js @@ -27,53 +27,52 @@ function calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipm Equipment: ${equipment?.categoryName} (width: ${equipment?.sprayWidthFeet || 'N/A'} ft) Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`); - // Calculate application speed first based on equipment and nozzle - if (equipment && nozzle && nozzle.flowRateGpm && equipment.sprayWidthFeet) { - const sprayWidthFeet = equipment.sprayWidthFeet; - const nozzleGpm = nozzle.flowRateGpm; - - // For liquid applications, we need to determine optimal speed based on target carrier rate - // Let's use a reasonable default carrier rate of 20 GPA (gallons per acre) - const targetGallonsPerAcre = 20; - const targetGallonsPerSqft = targetGallonsPerAcre / 43560; - - // Formula: Speed (MPH) = GPM / (width_ft × target_rate_gal_per_sqft × 88) - // Where 88 converts MPH to ft/min - applicationSpeedMph = nozzleGpm / (sprayWidthFeet * targetGallonsPerSqft * 88); - - // Limit speed to reasonable range (1-8 MPH for lawn applications) - applicationSpeedMph = Math.max(1, Math.min(8, applicationSpeedMph)); - } else { - applicationSpeedMph = 3; // Default speed - } + // Use a practical application speed for lawn treatments + applicationSpeedMph = 3; // Standard walking speed for most lawn applications - // Now calculate actual carrier (water) rate using the calculated speed + // Calculate actual carrier (water) rate based on equipment specs and application speed // Formula: Rate (gal/sqft) = flow (GPM) / (width (ft) × speed (ft/min)) // Where speed (ft/min) = speed (MPH) × 88 let carrierRateGalPerSqft = 0; + let actualGallonsPerAcre = 0; if (equipment && nozzle && nozzle.flowRateGpm && equipment.sprayWidthFeet) { const flowGpm = nozzle.flowRateGpm; const widthFt = equipment.sprayWidthFeet; const speedFtPerMin = applicationSpeedMph * 88; // Convert MPH to ft/min + // Calculate carrier rate based on actual equipment parameters carrierRateGalPerSqft = flowGpm / (widthFt * speedFtPerMin); waterGallons = carrierRateGalPerSqft * areaSquareFeet; - console.log('Carrier rate calculation:', { + // Calculate the resulting GPA for reference + actualGallonsPerAcre = carrierRateGalPerSqft * 43560; + + console.log('Equipment-based carrier rate calculation:', { flowGpm, widthFt, speedMph: applicationSpeedMph, speedFtPerMin, - carrierRateGalPerSqft, + carrierRateGalPerSqft: carrierRateGalPerSqft.toFixed(6), areaSquareFeet, - totalWaterGallons: waterGallons, - carrierRateGalPer1000Sqft: carrierRateGalPerSqft * 1000 + totalWaterGallons: waterGallons.toFixed(2), + resultingGPA: actualGallonsPerAcre.toFixed(1), + carrierRateGalPer1000Sqft: (carrierRateGalPerSqft * 1000).toFixed(3) }); } else { - // Fallback to area-based calculation if equipment data is missing + // Fallback when equipment/nozzle data is missing console.log('Using fallback water calculation - missing equipment/nozzle data'); - waterGallons = area1000sqft * 0.164; // Use your example: 0.164 gal/1000sqft + // Use a reasonable default rate for lawn applications (25 GPA) + const fallbackGPA = 25; + const fallbackRatePerSqft = fallbackGPA / 43560; + waterGallons = fallbackRatePerSqft * areaSquareFeet; + actualGallonsPerAcre = fallbackGPA; + + console.log('Fallback calculation:', { + fallbackGPA, + areaSquareFeet, + totalWaterGallons: waterGallons.toFixed(2) + }); } // Calculate product amount based on rate unit diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index f382fb8..ab23b21 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -322,7 +322,12 @@ const Applications = () => { ) : ( <> -

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

+ {/* Show single product with name */} + {application.productDetails && application.productDetails.length === 1 ? ( +

• {application.productDetails[0].name}{application.productDetails[0].brand ? ` (${application.productDetails[0].brand})` : ''}: {application.productDetails[0].calculatedAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}

+ ) : ( +

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

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

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

)} @@ -835,18 +840,18 @@ const ApplicationPlanModal = ({
{ const newProducts = [...planData.selectedProducts]; newProducts[index] = { ...item, - rateAmount: parseFloat(e.target.value) || 0 + rateAmount: e.target.value === '' ? '' : parseFloat(e.target.value) }; setPlanData({ ...planData, selectedProducts: newProducts }); }} - className="w-20 px-2 py-1 text-sm border rounded" + className="w-24 px-2 py-1 text-sm border rounded" placeholder="Rate" />