multiarea
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
24
database/migrations/support_multiple_areas_per_plan.sql
Normal file
24
database/migrations/support_multiple_areas_per_plan.sql
Normal file
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user