GPA
This commit is contained in:
@@ -355,29 +355,54 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
|
|
||||||
const plan = planResult.rows[0];
|
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
|
// Add products to plan with calculations
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
const { productId, userProductId, rateAmount, rateUnit, applicationType } = product;
|
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
|
// Prepare equipment object for calculations
|
||||||
const equipmentForCalc = {
|
const equipmentForCalc = {
|
||||||
categoryName: equipmentData.category_name,
|
categoryName: equipmentData.category_name,
|
||||||
@@ -393,7 +418,7 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
sprayAngle: nozzleData.spray_angle
|
sprayAngle: nozzleData.spray_angle
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
// Perform advanced calculations using the calculation engine
|
// Calculate product amount
|
||||||
const calculations = calculateApplication({
|
const calculations = calculateApplication({
|
||||||
areaSquareFeet: sectionArea,
|
areaSquareFeet: sectionArea,
|
||||||
rateAmount: parseFloat(rateAmount),
|
rateAmount: parseFloat(rateAmount),
|
||||||
@@ -403,7 +428,12 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
nozzle: nozzleForCalc
|
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
|
// Extract calculated values based on application type
|
||||||
let calculatedProductAmount = 0;
|
let calculatedProductAmount = 0;
|
||||||
@@ -412,7 +442,9 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
|
|
||||||
if (calculations.type === 'liquid') {
|
if (calculations.type === 'liquid') {
|
||||||
calculatedProductAmount = calculations.productAmountOunces || 0;
|
calculatedProductAmount = calculations.productAmountOunces || 0;
|
||||||
calculatedWaterAmount = calculations.waterAmountGallons || 0;
|
// Use shared water amount for liquid applications
|
||||||
|
calculatedWaterAmount = sharedWaterAmount;
|
||||||
|
targetSpeed = sharedTargetSpeed;
|
||||||
} else if (calculations.type === 'granular') {
|
} else if (calculations.type === 'granular') {
|
||||||
calculatedProductAmount = calculations.productAmountPounds || 0;
|
calculatedProductAmount = calculations.productAmountPounds || 0;
|
||||||
calculatedWaterAmount = 0; // No water for granular
|
calculatedWaterAmount = 0; // No water for granular
|
||||||
@@ -565,12 +597,47 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat
|
|||||||
// Delete existing products
|
// Delete existing products
|
||||||
await client.query('DELETE FROM application_plan_products WHERE plan_id = $1', [planId]);
|
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
|
// Add updated products with recalculation
|
||||||
for (const product of products) {
|
for (const product of products) {
|
||||||
const { productId, userProductId, rateAmount, rateUnit, applicationType } = product;
|
const { productId, userProductId, rateAmount, rateUnit, applicationType } = product;
|
||||||
|
|
||||||
const sectionArea = areaSquareFeet || parseFloat(section.area);
|
|
||||||
|
|
||||||
// Prepare equipment object for calculations
|
// Prepare equipment object for calculations
|
||||||
const equipmentForCalc = {
|
const equipmentForCalc = {
|
||||||
categoryName: equipmentData.category_name,
|
categoryName: equipmentData.category_name,
|
||||||
@@ -603,7 +670,9 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat
|
|||||||
|
|
||||||
if (calculations.type === 'liquid') {
|
if (calculations.type === 'liquid') {
|
||||||
calculatedProductAmount = calculations.productAmountOunces || 0;
|
calculatedProductAmount = calculations.productAmountOunces || 0;
|
||||||
calculatedWaterAmount = calculations.waterAmountGallons || 0;
|
// Use shared water amount for liquid applications
|
||||||
|
calculatedWaterAmount = sharedWaterAmount;
|
||||||
|
targetSpeed = sharedTargetSpeed;
|
||||||
} else if (calculations.type === 'granular') {
|
} else if (calculations.type === 'granular') {
|
||||||
calculatedProductAmount = calculations.productAmountPounds || 0;
|
calculatedProductAmount = calculations.productAmountPounds || 0;
|
||||||
calculatedWaterAmount = 0;
|
calculatedWaterAmount = 0;
|
||||||
|
|||||||
@@ -27,53 +27,52 @@ function calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipm
|
|||||||
Equipment: ${equipment?.categoryName} (width: ${equipment?.sprayWidthFeet || 'N/A'} ft)
|
Equipment: ${equipment?.categoryName} (width: ${equipment?.sprayWidthFeet || 'N/A'} ft)
|
||||||
Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`);
|
Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`);
|
||||||
|
|
||||||
// Calculate application speed first based on equipment and nozzle
|
// Use a practical application speed for lawn treatments
|
||||||
if (equipment && nozzle && nozzle.flowRateGpm && equipment.sprayWidthFeet) {
|
applicationSpeedMph = 3; // Standard walking speed for most lawn applications
|
||||||
const sprayWidthFeet = equipment.sprayWidthFeet;
|
|
||||||
const nozzleGpm = nozzle.flowRateGpm;
|
|
||||||
|
|
||||||
// For liquid applications, we need to determine optimal speed based on target carrier rate
|
// Calculate actual carrier (water) rate based on equipment specs and application speed
|
||||||
// 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
|
|
||||||
// Formula: Rate (gal/sqft) = flow (GPM) / (width (ft) × speed (ft/min))
|
// Formula: Rate (gal/sqft) = flow (GPM) / (width (ft) × speed (ft/min))
|
||||||
// Where speed (ft/min) = speed (MPH) × 88
|
// Where speed (ft/min) = speed (MPH) × 88
|
||||||
let carrierRateGalPerSqft = 0;
|
let carrierRateGalPerSqft = 0;
|
||||||
|
let actualGallonsPerAcre = 0;
|
||||||
|
|
||||||
if (equipment && nozzle && nozzle.flowRateGpm && equipment.sprayWidthFeet) {
|
if (equipment && nozzle && nozzle.flowRateGpm && equipment.sprayWidthFeet) {
|
||||||
const flowGpm = nozzle.flowRateGpm;
|
const flowGpm = nozzle.flowRateGpm;
|
||||||
const widthFt = equipment.sprayWidthFeet;
|
const widthFt = equipment.sprayWidthFeet;
|
||||||
const speedFtPerMin = applicationSpeedMph * 88; // Convert MPH to ft/min
|
const speedFtPerMin = applicationSpeedMph * 88; // Convert MPH to ft/min
|
||||||
|
|
||||||
|
// Calculate carrier rate based on actual equipment parameters
|
||||||
carrierRateGalPerSqft = flowGpm / (widthFt * speedFtPerMin);
|
carrierRateGalPerSqft = flowGpm / (widthFt * speedFtPerMin);
|
||||||
waterGallons = carrierRateGalPerSqft * areaSquareFeet;
|
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,
|
flowGpm,
|
||||||
widthFt,
|
widthFt,
|
||||||
speedMph: applicationSpeedMph,
|
speedMph: applicationSpeedMph,
|
||||||
speedFtPerMin,
|
speedFtPerMin,
|
||||||
carrierRateGalPerSqft,
|
carrierRateGalPerSqft: carrierRateGalPerSqft.toFixed(6),
|
||||||
areaSquareFeet,
|
areaSquareFeet,
|
||||||
totalWaterGallons: waterGallons,
|
totalWaterGallons: waterGallons.toFixed(2),
|
||||||
carrierRateGalPer1000Sqft: carrierRateGalPerSqft * 1000
|
resultingGPA: actualGallonsPerAcre.toFixed(1),
|
||||||
|
carrierRateGalPer1000Sqft: (carrierRateGalPerSqft * 1000).toFixed(3)
|
||||||
});
|
});
|
||||||
} else {
|
} 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');
|
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
|
// Calculate product amount based on rate unit
|
||||||
|
|||||||
@@ -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>
|
<p>• Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
|
||||||
|
)}
|
||||||
{application.totalWaterAmount > 0 && (
|
{application.totalWaterAmount > 0 && (
|
||||||
<p>• Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
|
<p>• Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
|
||||||
)}
|
)}
|
||||||
@@ -835,18 +840,18 @@ const ApplicationPlanModal = ({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.1"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
value={item.rateAmount || ''}
|
value={item.rateAmount || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newProducts = [...planData.selectedProducts];
|
const newProducts = [...planData.selectedProducts];
|
||||||
newProducts[index] = {
|
newProducts[index] = {
|
||||||
...item,
|
...item,
|
||||||
rateAmount: parseFloat(e.target.value) || 0
|
rateAmount: e.target.value === '' ? '' : parseFloat(e.target.value)
|
||||||
};
|
};
|
||||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
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"
|
placeholder="Rate"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
|
|||||||
Reference in New Issue
Block a user