diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index b328bf7..6e675c9 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -42,7 +42,10 @@ router.get('/plans', async (req, res, next) => { `SELECT ap.*, ls.name as section_name, ls.area as section_area, p.name as property_name, p.address as property_address, ue.custom_name as equipment_name, et.name as equipment_type, - COUNT(app.id) as product_count + COUNT(app.id) as product_count, + SUM(app.calculated_product_amount) as total_product_amount, + SUM(app.calculated_water_amount) as total_water_amount, + AVG(app.target_speed_mph) as avg_speed_mph FROM application_plans ap JOIN lawn_sections ls ON ap.lawn_section_id = ls.id JOIN properties p ON ls.property_id = p.id @@ -69,6 +72,9 @@ router.get('/plans', async (req, res, next) => { propertyAddress: plan.property_address, equipmentName: plan.equipment_name || plan.equipment_type, productCount: parseInt(plan.product_count), + totalProductAmount: parseFloat(plan.total_product_amount || 0), + totalWaterAmount: parseFloat(plan.total_water_amount || 0), + avgSpeedMph: parseFloat(plan.avg_speed_mph || 0), createdAt: plan.created_at, updatedAt: plan.updated_at })) @@ -207,9 +213,13 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n const section = sectionCheck.rows[0]; - // Verify equipment belongs to user + // Verify equipment belongs to user and get equipment details with category const equipmentCheck = await client.query( - 'SELECT id, tank_size, pump_gpm, nozzle_gpm, nozzle_count FROM user_equipment WHERE id = $1 AND user_id = $2', + `SELECT ue.*, et.name as type_name, ec.name as category_name + FROM user_equipment ue + LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id + LEFT JOIN equipment_categories ec ON COALESCE(et.category_id, ue.category_id) = ec.id + WHERE ue.id = $1 AND ue.user_id = $2`, [equipmentId, req.user.id] ); @@ -217,7 +227,20 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n throw new AppError('Equipment not found', 404); } - const equipment = equipmentCheck.rows[0]; + const equipmentData = equipmentCheck.rows[0]; + + // Get nozzle data if provided + let nozzleData = null; + if (nozzleId) { + const nozzleCheck = await client.query( + 'SELECT * FROM user_equipment WHERE id = $1 AND user_id = $2', + [nozzleId, req.user.id] + ); + + if (nozzleCheck.rows.length > 0) { + nozzleData = nozzleCheck.rows[0]; + } + } console.log('Creating plan with data:', { userId: req.user.id, @@ -245,14 +268,38 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n // 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, + nozzleData + }); + + // Prepare equipment object for calculations + const equipmentForCalc = { + categoryName: equipmentData.category_name, + tankSizeGallons: equipmentData.tank_size_gallons, + sprayWidthFeet: equipmentData.spray_width_feet, + capacityLbs: equipmentData.capacity_lbs, + spreadWidth: equipmentData.spread_width + }; + + // Prepare nozzle object for calculations + const nozzleForCalc = nozzleData ? { + flowRateGpm: nozzleData.flow_rate_gpm, + sprayAngle: nozzleData.spray_angle + } : null; + // Perform advanced calculations using the calculation engine const calculations = calculateApplication({ areaSquareFeet: sectionArea, rateAmount: parseFloat(rateAmount), rateUnit, applicationType, - equipment, - nozzle + equipment: equipmentForCalc, + nozzle: nozzleForCalc }); console.log('Plan creation calculations:', calculations); @@ -282,6 +329,16 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n await client.query('COMMIT'); + // Get the created plan with calculations for response + const createdPlanResult = await client.query( + `SELECT ap.*, app.calculated_product_amount, app.calculated_water_amount, app.target_speed_mph, + app.rate_amount, app.rate_unit + FROM application_plans ap + LEFT JOIN application_plan_products app ON ap.id = app.plan_id + WHERE ap.id = $1`, + [plan.id] + ); + res.status(201).json({ success: true, message: 'Application plan created successfully', @@ -291,7 +348,14 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n status: plan.status, plannedDate: plan.planned_date, notes: plan.notes, - createdAt: plan.created_at + createdAt: plan.created_at, + calculations: createdPlanResult.rows.map(row => ({ + productAmount: row.calculated_product_amount, + waterAmount: row.calculated_water_amount, + targetSpeed: row.target_speed_mph, + rateAmount: row.rate_amount, + rateUnit: row.rate_unit + })) } } }); @@ -308,6 +372,178 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n } }); +// @route PUT /api/applications/plans/:id +// @desc Update application plan +// @access Private +router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicationPlanSchema), async (req, res, next) => { + try { + const planId = req.params.id; + const { + lawnSectionId, + equipmentId, + nozzleId, + plannedDate, + notes, + products, + areaSquareFeet, + equipment, + nozzle + } = req.body; + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Check if plan belongs to user + const planCheck = await client.query( + 'SELECT id FROM application_plans WHERE id = $1 AND user_id = $2', + [planId, req.user.id] + ); + + if (planCheck.rows.length === 0) { + throw new AppError('Application plan not found', 404); + } + + // Verify lawn section belongs to user + const sectionCheck = await client.query( + `SELECT ls.id, ls.area, p.user_id + FROM lawn_sections ls + JOIN properties p ON ls.property_id = p.id + WHERE ls.id = $1 AND p.user_id = $2`, + [lawnSectionId, req.user.id] + ); + + if (sectionCheck.rows.length === 0) { + throw new AppError('Lawn section not found', 404); + } + + const section = sectionCheck.rows[0]; + + // Verify equipment belongs to user and get equipment details + const equipmentCheck = await client.query( + `SELECT ue.*, et.name as type_name, ec.name as category_name + FROM user_equipment ue + LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id + LEFT JOIN equipment_categories ec ON COALESCE(et.category_id, ue.category_id) = ec.id + WHERE ue.id = $1 AND ue.user_id = $2`, + [equipmentId, req.user.id] + ); + + if (equipmentCheck.rows.length === 0) { + throw new AppError('Equipment not found', 404); + } + + const equipmentData = equipmentCheck.rows[0]; + + // Get nozzle data if provided + let nozzleData = null; + if (nozzleId) { + const nozzleCheck = await client.query( + 'SELECT * FROM user_equipment WHERE id = $1 AND user_id = $2', + [nozzleId, req.user.id] + ); + + if (nozzleCheck.rows.length > 0) { + nozzleData = nozzleCheck.rows[0]; + } + } + + // Update application plan + const updateResult = await client.query( + `UPDATE application_plans + SET lawn_section_id = $1, equipment_id = $2, nozzle_id = $3, + planned_date = $4, notes = $5, updated_at = CURRENT_TIMESTAMP + WHERE id = $6 + RETURNING *`, + [lawnSectionId, equipmentId, nozzleId, plannedDate, notes, planId] + ); + + const plan = updateResult.rows[0]; + + // Delete existing products + await client.query('DELETE FROM application_plan_products WHERE plan_id = $1', [planId]); + + // 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, + tankSizeGallons: equipmentData.tank_size_gallons, + sprayWidthFeet: equipmentData.spray_width_feet, + capacityLbs: equipmentData.capacity_lbs, + spreadWidth: equipmentData.spread_width + }; + + // Prepare nozzle object for calculations + const nozzleForCalc = nozzleData ? { + flowRateGpm: nozzleData.flow_rate_gpm, + sprayAngle: nozzleData.spray_angle + } : null; + + // Perform recalculation + const calculations = calculateApplication({ + areaSquareFeet: sectionArea, + rateAmount: parseFloat(rateAmount), + rateUnit, + applicationType, + equipment: equipmentForCalc, + nozzle: nozzleForCalc + }); + + // Extract calculated values + let calculatedProductAmount = 0; + let calculatedWaterAmount = 0; + let targetSpeed = calculations.applicationSpeedMph || 3; + + if (calculations.type === 'liquid') { + calculatedProductAmount = calculations.productAmountOunces || 0; + calculatedWaterAmount = calculations.waterAmountGallons || 0; + } else if (calculations.type === 'granular') { + calculatedProductAmount = calculations.productAmountPounds || 0; + calculatedWaterAmount = 0; + } + + await client.query( + `INSERT INTO application_plan_products + (plan_id, product_id, user_product_id, rate_amount, rate_unit, + calculated_product_amount, calculated_water_amount, target_speed_mph) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, + [plan.id, productId, userProductId, rateAmount, rateUnit, + calculatedProductAmount, calculatedWaterAmount, targetSpeed] + ); + } + + await client.query('COMMIT'); + + res.json({ + success: true, + message: 'Application plan updated successfully', + data: { + plan: { + id: plan.id, + status: plan.status, + plannedDate: plan.planned_date, + notes: plan.notes, + updatedAt: plan.updated_at + } + } + }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error) { + next(error); + } +}); + // @route PUT /api/applications/plans/:id/status // @desc Update application plan status // @access Private diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index 97d7320..379b95d 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -156,9 +156,22 @@ const Applications = () => {

Equipment: {application.equipmentName}

-

+

Products: {application.productCount}

+ {/* Display calculated amounts */} + {application.totalProductAmount > 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.notes && (

"{application.notes}" diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 323dc71..6781b10 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -151,6 +151,7 @@ export const applicationsAPI = { getPlans: (params) => apiClient.get('/applications/plans', { params }), getPlan: (id) => apiClient.get(`/applications/plans/${id}`), createPlan: (planData) => apiClient.post('/applications/plans', planData), + updatePlan: (id, planData) => apiClient.put(`/applications/plans/${id}`, planData), updatePlanStatus: (id, status) => apiClient.put(`/applications/plans/${id}/status`, { status }), // Logs