From 7983503e5e09973bb4e47749ae8dea494c9274e7 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Tue, 26 Aug 2025 07:46:37 -0500 Subject: [PATCH] multiarea --- backend/src/routes/applications.js | 156 +++++--- backend/src/utils/validation.js | 5 +- .../support_multiple_areas_per_plan.sql | 24 ++ .../src/pages/Applications/Applications.js | 340 ++++++------------ 4 files changed, 251 insertions(+), 274 deletions(-) create mode 100644 database/migrations/support_multiple_areas_per_plan.sql diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index 6bcd46e..745b56d 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -70,21 +70,25 @@ router.get('/plans', async (req, res, next) => { const whereClause = whereConditions.join(' AND '); const result = await pool.query( - `SELECT ap.*, ls.name as section_name, ls.area as section_area, + `SELECT ap.*, + STRING_AGG(DISTINCT ls.name, ', ') as section_names, + SUM(ls.area) as total_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(DISTINCT app.id) as product_count, SUM(app.calculated_product_amount) as total_product_amount, MAX(app.calculated_water_amount) as total_water_amount, - AVG(app.target_speed_mph) as avg_speed_mph + AVG(app.target_speed_mph) as avg_speed_mph, + COUNT(DISTINCT aps.lawn_section_id) as section_count FROM application_plans ap - JOIN lawn_sections ls ON ap.lawn_section_id = ls.id + JOIN application_plan_sections aps ON ap.id = aps.plan_id + JOIN lawn_sections ls ON aps.lawn_section_id = ls.id JOIN properties p ON ls.property_id = p.id LEFT JOIN user_equipment ue ON ap.equipment_id = ue.id LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id LEFT JOIN application_plan_products app ON ap.id = app.plan_id WHERE ${whereClause} - GROUP BY ap.id, ls.name, ls.area, p.name, p.address, ue.custom_name, et.name + GROUP BY ap.id, p.name, p.address, ue.custom_name, et.name ORDER BY ap.planned_date DESC, ap.created_at DESC`, queryParams ); @@ -176,8 +180,9 @@ router.get('/plans', async (req, res, next) => { status: plan.status, plannedDate: plan.planned_date, notes: plan.notes, - sectionName: plan.section_name, - sectionArea: parseFloat(plan.section_area), + sectionNames: plan.section_names, // Multiple section names comma-separated + sectionCount: parseInt(plan.section_count), + totalSectionArea: parseFloat(plan.total_section_area), propertyName: plan.property_name, propertyAddress: plan.property_address, equipmentName: plan.equipment_name || plan.equipment_type, @@ -211,23 +216,35 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) = try { const planId = req.params.id; - // Get plan details + // Get plan details with all sections const planResult = await pool.query( - `SELECT ap.*, ls.name as section_name, ls.area as section_area, ls.polygon_data, + `SELECT ap.*, p.id as property_id, p.name as property_name, p.address as property_address, ue.id as equipment_id, ue.custom_name as equipment_name, et.name as equipment_type, et.category as equipment_category, nz.id as nozzle_id, nz.custom_name as nozzle_name, nz.flow_rate_gpm, nz.spray_angle FROM application_plans ap - JOIN lawn_sections ls ON ap.lawn_section_id = ls.id + JOIN application_plan_sections aps ON ap.id = aps.plan_id + JOIN lawn_sections ls ON aps.lawn_section_id = ls.id JOIN properties p ON ls.property_id = p.id LEFT JOIN user_equipment ue ON ap.equipment_id = ue.id LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id LEFT JOIN user_equipment nz ON ap.nozzle_id = nz.id - WHERE ap.id = $1 AND ap.user_id = $2`, + WHERE ap.id = $1 AND ap.user_id = $2 + LIMIT 1`, [planId, req.user.id] ); + // Get sections for this plan separately + const sectionsResult = await pool.query( + `SELECT ls.id, ls.name, ls.area, ls.polygon_data + FROM application_plan_sections aps + JOIN lawn_sections ls ON aps.lawn_section_id = ls.id + WHERE aps.plan_id = $1 + ORDER BY ls.name`, + [planId] + ); + if (planResult.rows.length === 0) { throw new AppError('Application plan not found', 404); } @@ -248,6 +265,15 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) = [planId] ); + const sections = sectionsResult.rows.map(section => ({ + id: section.id, + name: section.name, + area: parseFloat(section.area), + polygonData: section.polygon_data + })); + + const totalArea = sections.reduce((sum, section) => sum + section.area, 0); + res.json({ success: true, data: { @@ -256,12 +282,8 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) = status: plan.status, plannedDate: plan.planned_date, notes: plan.notes, - section: { - id: plan.lawn_section_id, - name: plan.section_name, - area: parseFloat(plan.section_area), - polygonData: plan.polygon_data - }, + sections: sections, // Array of sections instead of single section + totalArea: totalArea, property: { id: plan.property_id, name: plan.property_name, @@ -309,6 +331,7 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n try { const { lawnSectionId, + lawnSectionIds, // New multi-area support equipmentId, nozzleId, plannedDate, @@ -319,26 +342,36 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n nozzle } = req.body; + // Handle both single and multiple lawn sections + const sectionIds = lawnSectionIds || [lawnSectionId]; + // Start transaction const client = await pool.connect(); try { await client.query('BEGIN'); - // Verify lawn section belongs to user + // Verify all lawn sections belong to user and are from same property const sectionCheck = await client.query( - `SELECT ls.id, ls.area, p.user_id + `SELECT ls.id, ls.area, p.user_id, p.id as property_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] + WHERE ls.id = ANY($1) AND p.user_id = $2`, + [sectionIds, req.user.id] ); - if (sectionCheck.rows.length === 0) { - throw new AppError('Lawn section not found', 404); + if (sectionCheck.rows.length !== sectionIds.length) { + throw new AppError('One or more lawn sections not found', 404); } - const section = sectionCheck.rows[0]; + // Ensure all sections are from the same property + const propertyIds = [...new Set(sectionCheck.rows.map(row => row.property_id))]; + if (propertyIds.length > 1) { + throw new AppError('All sections must be from the same property', 400); + } + + const sections = sectionCheck.rows; + const totalArea = areaSquareFeet || sections.reduce((sum, section) => sum + parseFloat(section.area), 0); // Verify equipment belongs to user and get equipment details with category const equipmentCheck = await client.query( @@ -371,25 +404,34 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n console.log('Creating plan with data:', { userId: req.user.id, - lawnSectionId, + sectionIds, equipmentId, nozzleId, plannedDate, notes }); - // Create application plan + // Create application plan (no longer has lawn_section_id column) const planResult = await client.query( - `INSERT INTO application_plans (user_id, lawn_section_id, equipment_id, nozzle_id, planned_date, notes) - VALUES ($1, $2, $3, $4, $5, $6) + `INSERT INTO application_plans (user_id, equipment_id, nozzle_id, planned_date, notes) + VALUES ($1, $2, $3, $4, $5) RETURNING *`, - [req.user.id, lawnSectionId, equipmentId, nozzleId, plannedDate, notes] + [req.user.id, equipmentId, nozzleId, plannedDate, notes] ); const plan = planResult.rows[0]; - // Calculate shared water amount and speed for liquid applications - const sectionArea = areaSquareFeet || parseFloat(section.area); + // Create section associations in junction table + for (const sectionId of sectionIds) { + await client.query( + `INSERT INTO application_plan_sections (plan_id, lawn_section_id) + VALUES ($1, $2)`, + [plan.id, sectionId] + ); + } + + // Calculate shared water amount and speed for liquid applications using total area + const sectionArea = totalArea; const firstProduct = products[0]; const isLiquid = firstProduct.applicationType === 'liquid'; @@ -539,13 +581,14 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n }); // @route PUT /api/applications/plans/:id -// @desc Update application plan +// @desc Update application plan (supports multiple areas) // @access Private router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicationPlanSchema), async (req, res, next) => { try { const planId = req.params.id; const { lawnSectionId, + lawnSectionIds, // New multi-area support equipmentId, nozzleId, plannedDate, @@ -556,6 +599,9 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat nozzle } = req.body; + // Handle both single and multiple lawn sections + const sectionIds = lawnSectionIds || [lawnSectionId]; + const client = await pool.connect(); try { @@ -571,20 +617,27 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat throw new AppError('Application plan not found', 404); } - // Verify lawn section belongs to user + // Verify all new lawn sections belong to user and are from same property const sectionCheck = await client.query( - `SELECT ls.id, ls.area, p.user_id + `SELECT ls.id, ls.area, p.user_id, p.id as property_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] + WHERE ls.id = ANY($1) AND p.user_id = $2`, + [sectionIds, req.user.id] ); - if (sectionCheck.rows.length === 0) { - throw new AppError('Lawn section not found', 404); + if (sectionCheck.rows.length !== sectionIds.length) { + throw new AppError('One or more lawn sections not found', 404); } - const section = sectionCheck.rows[0]; + // Ensure all sections are from the same property + const propertyIds = [...new Set(sectionCheck.rows.map(row => row.property_id))]; + if (propertyIds.length > 1) { + throw new AppError('All sections must be from the same property', 400); + } + + const sections = sectionCheck.rows; + const totalArea = areaSquareFeet || sections.reduce((sum, section) => sum + parseFloat(section.area), 0); // Verify equipment belongs to user and get equipment details const equipmentCheck = await client.query( @@ -615,23 +668,34 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat } } - // Update application plan + // Update application plan (no longer has lawn_section_id column) 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 + SET equipment_id = $1, nozzle_id = $2, + planned_date = $3, notes = $4, updated_at = CURRENT_TIMESTAMP + WHERE id = $5 RETURNING *`, - [lawnSectionId, equipmentId, nozzleId, plannedDate, notes, planId] + [equipmentId, nozzleId, plannedDate, notes, planId] ); const plan = updateResult.rows[0]; + // Update section associations - delete old ones and add new ones + await client.query('DELETE FROM application_plan_sections WHERE plan_id = $1', [planId]); + + for (const sectionId of sectionIds) { + await client.query( + `INSERT INTO application_plan_sections (plan_id, lawn_section_id) + VALUES ($1, $2)`, + [plan.id, sectionId] + ); + } + // 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); + // Calculate shared water amount and speed for liquid applications using total area + const sectionArea = totalArea; const firstProduct = products[0]; const isLiquid = firstProduct.applicationType === 'liquid'; diff --git a/backend/src/utils/validation.js b/backend/src/utils/validation.js index 3bd4235..a261255 100644 --- a/backend/src/utils/validation.js +++ b/backend/src/utils/validation.js @@ -123,7 +123,8 @@ const userProductSchema = Joi.object({ // Application validation schemas const applicationPlanSchema = Joi.object({ - lawnSectionId: Joi.number().integer().positive().required(), + lawnSectionId: Joi.number().integer().positive().optional(), // Keep for backward compatibility + lawnSectionIds: Joi.array().items(Joi.number().integer().positive()).min(1).optional(), // New multi-area support equipmentId: Joi.number().integer().positive().required(), nozzleId: Joi.number().integer().positive().optional(), plannedDate: Joi.date().required(), @@ -150,7 +151,7 @@ const applicationPlanSchema = Joi.object({ rateUnit: Joi.string().max(50).required(), applicationType: Joi.string().valid('liquid', 'granular').optional() })).min(1).required() -}); +}).or('lawnSectionId', 'lawnSectionIds'); // At least one lawn section parameter is required const applicationLogSchema = Joi.object({ planId: Joi.number().integer().positive(), diff --git a/database/migrations/support_multiple_areas_per_plan.sql b/database/migrations/support_multiple_areas_per_plan.sql new file mode 100644 index 0000000..b033d6d --- /dev/null +++ b/database/migrations/support_multiple_areas_per_plan.sql @@ -0,0 +1,24 @@ +-- Migration: Support multiple lawn sections per application plan +-- This allows a single plan to cover multiple areas of a property + +-- Create junction table for plan-section relationships +CREATE TABLE application_plan_sections ( + id SERIAL PRIMARY KEY, + plan_id INTEGER REFERENCES application_plans(id) ON DELETE CASCADE, + lawn_section_id INTEGER REFERENCES lawn_sections(id) ON DELETE CASCADE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(plan_id, lawn_section_id) +); + +-- Migrate existing data from application_plans.lawn_section_id to the junction table +INSERT INTO application_plan_sections (plan_id, lawn_section_id) +SELECT id, lawn_section_id +FROM application_plans +WHERE lawn_section_id IS NOT NULL; + +-- Remove the lawn_section_id column from application_plans (it's now in the junction table) +ALTER TABLE application_plans DROP COLUMN lawn_section_id; + +-- Create index for better performance on lookups +CREATE INDEX idx_application_plan_sections_plan_id ON application_plan_sections(plan_id); +CREATE INDEX idx_application_plan_sections_section_id ON application_plan_sections(lawn_section_id); \ No newline at end of file diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index b7d6a75..79b424a 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -398,231 +398,119 @@ const Applications = () => { onSubmit={async (planData) => { try { if (editingPlan) { - // Edit existing plan - handle multiple areas - if (planData.selectedAreas.length === 1) { - // Single area - update existing plan normally - const selectedArea = selectedPropertyDetails.sections.find(s => s.id === planData.selectedAreas[0]); - const areaSquareFeet = selectedArea?.area || 0; - const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); - const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; - - const planPayload = { - lawnSectionId: parseInt(planData.selectedAreas[0]), - equipmentId: parseInt(planData.equipmentId), - ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), - plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], - notes: planData.notes || '', - areaSquareFeet: areaSquareFeet, - equipment: { - id: selectedEquipment?.id, - categoryName: selectedEquipment?.categoryName, - tankSizeGallons: selectedEquipment?.tankSizeGallons, - pumpGpm: selectedEquipment?.pumpGpm, - sprayWidthFeet: selectedEquipment?.sprayWidthFeet, - capacityLbs: selectedEquipment?.capacityLbs, - spreadWidth: selectedEquipment?.spreadWidth - }, - ...(planData.applicationType === 'liquid' && selectedNozzle && { - nozzle: { - id: selectedNozzle.id, - flowRateGpm: selectedNozzle.flowRateGpm, - sprayAngle: selectedNozzle.sprayAngle - } - }), - 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); - toast.success('Application plan updated successfully'); - } else { - // Multiple areas - update existing plan for first area, create new plans for additional areas - const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); - const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; - - // Calculate total area for all selected areas (for quantity calculations) - const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => { - const area = selectedPropertyDetails.sections.find(s => s.id === areaId); - return total + (area?.area || 0); - }, 0); - - // Update existing plan with first selected area but use total area for calculations - const updatePayload = { - lawnSectionId: parseInt(planData.selectedAreas[0]), - equipmentId: parseInt(planData.equipmentId), - ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), - plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], - notes: planData.notes || '', - areaSquareFeet: totalAreaSquareFeet, // Use total area for proper quantity calculation - equipment: { - id: selectedEquipment?.id, - categoryName: selectedEquipment?.categoryName, - tankSizeGallons: selectedEquipment?.tankSizeGallons, - pumpGpm: selectedEquipment?.pumpGpm, - sprayWidthFeet: selectedEquipment?.sprayWidthFeet, - capacityLbs: selectedEquipment?.capacityLbs, - spreadWidth: selectedEquipment?.spreadWidth - }, - ...(planData.applicationType === 'liquid' && selectedNozzle && { - nozzle: { - id: selectedNozzle.id, - flowRateGpm: selectedNozzle.flowRateGpm, - sprayAngle: selectedNozzle.sprayAngle - } - }), - 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, updatePayload); - - // Create new plans for additional areas - const additionalAreas = planData.selectedAreas.slice(1); - const additionalPlanPromises = additionalAreas.map(async (areaId) => { - const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId); - const areaSquareFeet = selectedArea?.area || 0; - - const planPayload = { - lawnSectionId: parseInt(areaId), - equipmentId: parseInt(planData.equipmentId), - ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), - plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], - notes: planData.notes || '', - areaSquareFeet: areaSquareFeet, // Individual area for this plan - equipment: { - id: selectedEquipment?.id, - categoryName: selectedEquipment?.categoryName, - tankSizeGallons: selectedEquipment?.tankSizeGallons, - pumpGpm: selectedEquipment?.pumpGpm, - sprayWidthFeet: selectedEquipment?.sprayWidthFeet, - capacityLbs: selectedEquipment?.capacityLbs, - spreadWidth: selectedEquipment?.spreadWidth - }, - ...(planData.applicationType === 'liquid' && selectedNozzle && { - nozzle: { - id: selectedNozzle.id, - flowRateGpm: selectedNozzle.flowRateGpm, - sprayAngle: selectedNozzle.sprayAngle - } - }), - 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); - }); - - await Promise.all(additionalPlanPromises); - toast.success(`Application plan updated and ${additionalAreas.length} additional plan(s) created for new areas`); - } - } else { - // Create new plan(s) - const planPromises = planData.selectedAreas.map(async (areaId) => { - const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId); - const areaSquareFeet = selectedArea?.area || 0; - const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); - const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; - - const planPayload = { - lawnSectionId: parseInt(areaId), - equipmentId: parseInt(planData.equipmentId), - ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), - plannedDate: new Date().toISOString().split('T')[0], - notes: planData.notes || '', - areaSquareFeet: areaSquareFeet, - equipment: { - id: selectedEquipment?.id, - categoryName: selectedEquipment?.categoryName, - tankSizeGallons: selectedEquipment?.tankSizeGallons, - pumpGpm: selectedEquipment?.pumpGpm, - sprayWidthFeet: selectedEquipment?.sprayWidthFeet, - capacityLbs: selectedEquipment?.capacityLbs, - spreadWidth: selectedEquipment?.spreadWidth - }, - ...(planData.applicationType === 'liquid' && selectedNozzle && { - nozzle: { - id: selectedNozzle.id, - flowRateGpm: selectedNozzle.flowRateGpm, - sprayAngle: selectedNozzle.sprayAngle - } - }), - 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); - }); + // Edit existing plan - backend now supports multiple areas natively + const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); + const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; - await Promise.all(planPromises); - toast.success(`Created ${planData.selectedAreas.length} application plan(s) successfully`); + // Calculate total area for all selected areas + const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => { + const area = selectedPropertyDetails.sections.find(s => s.id === areaId); + return total + (area?.area || 0); + }, 0); + + const planPayload = { + lawnSectionIds: planData.selectedAreas.map(id => parseInt(id)), // Send multiple areas + equipmentId: parseInt(planData.equipmentId), + ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), + plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], + notes: planData.notes || '', + areaSquareFeet: totalAreaSquareFeet, + equipment: { + id: selectedEquipment?.id, + categoryName: selectedEquipment?.categoryName, + tankSizeGallons: selectedEquipment?.tankSizeGallons, + pumpGpm: selectedEquipment?.pumpGpm, + sprayWidthFeet: selectedEquipment?.sprayWidthFeet, + capacityLbs: selectedEquipment?.capacityLbs, + spreadWidth: selectedEquipment?.spreadWidth + }, + ...(planData.applicationType === 'liquid' && selectedNozzle && { + nozzle: { + id: selectedNozzle.id, + flowRateGpm: selectedNozzle.flowRateGpm, + sprayAngle: selectedNozzle.sprayAngle + } + }), + 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); + toast.success(`Application plan updated successfully for ${planData.selectedAreas.length} area(s)`); + } else { + // Create new plan - backend now supports multiple areas in single plan + const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); + const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; + + // Calculate total area for all selected areas + const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => { + const area = selectedPropertyDetails.sections.find(s => s.id === areaId); + return total + (area?.area || 0); + }, 0); + + const planPayload = { + lawnSectionIds: planData.selectedAreas.map(id => parseInt(id)), // Send multiple areas + equipmentId: parseInt(planData.equipmentId), + ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), + plannedDate: new Date().toISOString().split('T')[0], + notes: planData.notes || '', + areaSquareFeet: totalAreaSquareFeet, + equipment: { + id: selectedEquipment?.id, + categoryName: selectedEquipment?.categoryName, + tankSizeGallons: selectedEquipment?.tankSizeGallons, + pumpGpm: selectedEquipment?.pumpGpm, + sprayWidthFeet: selectedEquipment?.sprayWidthFeet, + capacityLbs: selectedEquipment?.capacityLbs, + spreadWidth: selectedEquipment?.spreadWidth + }, + ...(planData.applicationType === 'liquid' && selectedNozzle && { + nozzle: { + id: selectedNozzle.id, + flowRateGpm: selectedNozzle.flowRateGpm, + sprayAngle: selectedNozzle.sprayAngle + } + }), + 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.createPlan(planPayload); + toast.success(`Created application plan for ${planData.selectedAreas.length} area(s) successfully`); } setShowPlanForm(false); @@ -720,7 +608,7 @@ const ApplicationPlanModal = ({ setPlanData({ propertyId: propertyId?.toString() || '', - selectedAreas: [editingPlan.section?.id].filter(Boolean), // Allow adding more areas + selectedAreas: editingPlan.sections?.map(s => s.id) || [], // Handle multiple areas productId: selectedProduct?.uniqueId || '', selectedProduct: selectedProduct, selectedProducts: [], @@ -749,7 +637,7 @@ const ApplicationPlanModal = ({ setPlanData({ propertyId: propertyId?.toString() || '', - selectedAreas: [editingPlan.section?.id].filter(Boolean), // Allow adding more areas + selectedAreas: editingPlan.sections?.map(s => s.id) || [], // Handle multiple areas productId: '', selectedProduct: null, selectedProducts: selectedProducts,