This commit is contained in:
Jake Kasper
2025-08-26 07:09:25 -05:00
parent 054f743e2d
commit 0a47dd742b
3 changed files with 128 additions and 55 deletions

View File

@@ -355,29 +355,54 @@ 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 = {
categoryName: equipmentData.category_name,
@@ -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,12 +597,47 @@ 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 = {
categoryName: equipmentData.category_name,
@@ -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;

View File

@@ -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;
// Use a practical application speed for lawn treatments
applicationSpeedMph = 3; // Standard walking speed for most lawn applications
// 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
}
// 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

View File

@@ -322,7 +322,12 @@ const Applications = () => {
</>
) : (
<>
{/* Show single product with name */}
{application.productDetails && application.productDetails.length === 1 ? (
<p> {application.productDetails[0].name}{application.productDetails[0].brand ? ` (${application.productDetails[0].brand})` : ''}: {application.productDetails[0].calculatedAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
) : (
<p> Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
)}
{application.totalWaterAmount > 0 && (
<p> Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
)}
@@ -835,18 +840,18 @@ const ApplicationPlanModal = ({
<div className="flex items-center gap-2">
<input
type="number"
step="0.1"
step="0.01"
min="0"
value={item.rateAmount || ''}
onChange={(e) => {
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"
/>
<select