multiarea
This commit is contained in:
@@ -70,21 +70,25 @@ router.get('/plans', async (req, res, next) => {
|
|||||||
const whereClause = whereConditions.join(' AND ');
|
const whereClause = whereConditions.join(' AND ');
|
||||||
|
|
||||||
const result = await pool.query(
|
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,
|
p.name as property_name, p.address as property_address,
|
||||||
ue.custom_name as equipment_name, et.name as equipment_type,
|
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,
|
SUM(app.calculated_product_amount) as total_product_amount,
|
||||||
MAX(app.calculated_water_amount) as total_water_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
|
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
|
JOIN properties p ON ls.property_id = p.id
|
||||||
LEFT JOIN user_equipment ue ON ap.equipment_id = ue.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 equipment_types et ON ue.equipment_type_id = et.id
|
||||||
LEFT JOIN application_plan_products app ON ap.id = app.plan_id
|
LEFT JOIN application_plan_products app ON ap.id = app.plan_id
|
||||||
WHERE ${whereClause}
|
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`,
|
ORDER BY ap.planned_date DESC, ap.created_at DESC`,
|
||||||
queryParams
|
queryParams
|
||||||
);
|
);
|
||||||
@@ -176,8 +180,9 @@ router.get('/plans', async (req, res, next) => {
|
|||||||
status: plan.status,
|
status: plan.status,
|
||||||
plannedDate: plan.planned_date,
|
plannedDate: plan.planned_date,
|
||||||
notes: plan.notes,
|
notes: plan.notes,
|
||||||
sectionName: plan.section_name,
|
sectionNames: plan.section_names, // Multiple section names comma-separated
|
||||||
sectionArea: parseFloat(plan.section_area),
|
sectionCount: parseInt(plan.section_count),
|
||||||
|
totalSectionArea: parseFloat(plan.total_section_area),
|
||||||
propertyName: plan.property_name,
|
propertyName: plan.property_name,
|
||||||
propertyAddress: plan.property_address,
|
propertyAddress: plan.property_address,
|
||||||
equipmentName: plan.equipment_name || plan.equipment_type,
|
equipmentName: plan.equipment_name || plan.equipment_type,
|
||||||
@@ -211,23 +216,35 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
|||||||
try {
|
try {
|
||||||
const planId = req.params.id;
|
const planId = req.params.id;
|
||||||
|
|
||||||
// Get plan details
|
// Get plan details with all sections
|
||||||
const planResult = await pool.query(
|
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,
|
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,
|
ue.id as equipment_id, ue.custom_name as equipment_name,
|
||||||
et.name as equipment_type, et.category as equipment_category,
|
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
|
nz.id as nozzle_id, nz.custom_name as nozzle_name, nz.flow_rate_gpm, nz.spray_angle
|
||||||
FROM application_plans ap
|
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
|
JOIN properties p ON ls.property_id = p.id
|
||||||
LEFT JOIN user_equipment ue ON ap.equipment_id = ue.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 equipment_types et ON ue.equipment_type_id = et.id
|
||||||
LEFT JOIN user_equipment nz ON ap.nozzle_id = nz.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]
|
[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) {
|
if (planResult.rows.length === 0) {
|
||||||
throw new AppError('Application plan not found', 404);
|
throw new AppError('Application plan not found', 404);
|
||||||
}
|
}
|
||||||
@@ -248,6 +265,15 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
|||||||
[planId]
|
[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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -256,12 +282,8 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) =
|
|||||||
status: plan.status,
|
status: plan.status,
|
||||||
plannedDate: plan.planned_date,
|
plannedDate: plan.planned_date,
|
||||||
notes: plan.notes,
|
notes: plan.notes,
|
||||||
section: {
|
sections: sections, // Array of sections instead of single section
|
||||||
id: plan.lawn_section_id,
|
totalArea: totalArea,
|
||||||
name: plan.section_name,
|
|
||||||
area: parseFloat(plan.section_area),
|
|
||||||
polygonData: plan.polygon_data
|
|
||||||
},
|
|
||||||
property: {
|
property: {
|
||||||
id: plan.property_id,
|
id: plan.property_id,
|
||||||
name: plan.property_name,
|
name: plan.property_name,
|
||||||
@@ -309,6 +331,7 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
lawnSectionId,
|
lawnSectionId,
|
||||||
|
lawnSectionIds, // New multi-area support
|
||||||
equipmentId,
|
equipmentId,
|
||||||
nozzleId,
|
nozzleId,
|
||||||
plannedDate,
|
plannedDate,
|
||||||
@@ -319,26 +342,36 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
nozzle
|
nozzle
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
// Handle both single and multiple lawn sections
|
||||||
|
const sectionIds = lawnSectionIds || [lawnSectionId];
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
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(
|
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
|
FROM lawn_sections ls
|
||||||
JOIN properties p ON ls.property_id = p.id
|
JOIN properties p ON ls.property_id = p.id
|
||||||
WHERE ls.id = $1 AND p.user_id = $2`,
|
WHERE ls.id = ANY($1) AND p.user_id = $2`,
|
||||||
[lawnSectionId, req.user.id]
|
[sectionIds, req.user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sectionCheck.rows.length === 0) {
|
if (sectionCheck.rows.length !== sectionIds.length) {
|
||||||
throw new AppError('Lawn section not found', 404);
|
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
|
// Verify equipment belongs to user and get equipment details with category
|
||||||
const equipmentCheck = await client.query(
|
const equipmentCheck = await client.query(
|
||||||
@@ -371,25 +404,34 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
|
|
||||||
console.log('Creating plan with data:', {
|
console.log('Creating plan with data:', {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
lawnSectionId,
|
sectionIds,
|
||||||
equipmentId,
|
equipmentId,
|
||||||
nozzleId,
|
nozzleId,
|
||||||
plannedDate,
|
plannedDate,
|
||||||
notes
|
notes
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create application plan
|
// Create application plan (no longer has lawn_section_id column)
|
||||||
const planResult = await client.query(
|
const planResult = await client.query(
|
||||||
`INSERT INTO application_plans (user_id, lawn_section_id, equipment_id, nozzle_id, planned_date, notes)
|
`INSERT INTO application_plans (user_id, equipment_id, nozzle_id, planned_date, notes)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[req.user.id, lawnSectionId, equipmentId, nozzleId, plannedDate, notes]
|
[req.user.id, equipmentId, nozzleId, plannedDate, notes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = planResult.rows[0];
|
const plan = planResult.rows[0];
|
||||||
|
|
||||||
// Calculate shared water amount and speed for liquid applications
|
// Create section associations in junction table
|
||||||
const sectionArea = areaSquareFeet || parseFloat(section.area);
|
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 firstProduct = products[0];
|
||||||
const isLiquid = firstProduct.applicationType === 'liquid';
|
const isLiquid = firstProduct.applicationType === 'liquid';
|
||||||
|
|
||||||
@@ -539,13 +581,14 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
});
|
});
|
||||||
|
|
||||||
// @route PUT /api/applications/plans/:id
|
// @route PUT /api/applications/plans/:id
|
||||||
// @desc Update application plan
|
// @desc Update application plan (supports multiple areas)
|
||||||
// @access Private
|
// @access Private
|
||||||
router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicationPlanSchema), async (req, res, next) => {
|
router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicationPlanSchema), async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const planId = req.params.id;
|
const planId = req.params.id;
|
||||||
const {
|
const {
|
||||||
lawnSectionId,
|
lawnSectionId,
|
||||||
|
lawnSectionIds, // New multi-area support
|
||||||
equipmentId,
|
equipmentId,
|
||||||
nozzleId,
|
nozzleId,
|
||||||
plannedDate,
|
plannedDate,
|
||||||
@@ -556,6 +599,9 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat
|
|||||||
nozzle
|
nozzle
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
|
// Handle both single and multiple lawn sections
|
||||||
|
const sectionIds = lawnSectionIds || [lawnSectionId];
|
||||||
|
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -571,20 +617,27 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat
|
|||||||
throw new AppError('Application plan not found', 404);
|
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(
|
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
|
FROM lawn_sections ls
|
||||||
JOIN properties p ON ls.property_id = p.id
|
JOIN properties p ON ls.property_id = p.id
|
||||||
WHERE ls.id = $1 AND p.user_id = $2`,
|
WHERE ls.id = ANY($1) AND p.user_id = $2`,
|
||||||
[lawnSectionId, req.user.id]
|
[sectionIds, req.user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sectionCheck.rows.length === 0) {
|
if (sectionCheck.rows.length !== sectionIds.length) {
|
||||||
throw new AppError('Lawn section not found', 404);
|
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
|
// Verify equipment belongs to user and get equipment details
|
||||||
const equipmentCheck = await client.query(
|
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(
|
const updateResult = await client.query(
|
||||||
`UPDATE application_plans
|
`UPDATE application_plans
|
||||||
SET lawn_section_id = $1, equipment_id = $2, nozzle_id = $3,
|
SET equipment_id = $1, nozzle_id = $2,
|
||||||
planned_date = $4, notes = $5, updated_at = CURRENT_TIMESTAMP
|
planned_date = $3, notes = $4, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $6
|
WHERE id = $5
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[lawnSectionId, equipmentId, nozzleId, plannedDate, notes, planId]
|
[equipmentId, nozzleId, plannedDate, notes, planId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = updateResult.rows[0];
|
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
|
// 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
|
// Calculate shared water amount and speed for liquid applications using total area
|
||||||
const sectionArea = areaSquareFeet || parseFloat(section.area);
|
const sectionArea = totalArea;
|
||||||
const firstProduct = products[0];
|
const firstProduct = products[0];
|
||||||
const isLiquid = firstProduct.applicationType === 'liquid';
|
const isLiquid = firstProduct.applicationType === 'liquid';
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ const userProductSchema = Joi.object({
|
|||||||
|
|
||||||
// Application validation schemas
|
// Application validation schemas
|
||||||
const applicationPlanSchema = Joi.object({
|
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(),
|
equipmentId: Joi.number().integer().positive().required(),
|
||||||
nozzleId: Joi.number().integer().positive().optional(),
|
nozzleId: Joi.number().integer().positive().optional(),
|
||||||
plannedDate: Joi.date().required(),
|
plannedDate: Joi.date().required(),
|
||||||
@@ -150,7 +151,7 @@ const applicationPlanSchema = Joi.object({
|
|||||||
rateUnit: Joi.string().max(50).required(),
|
rateUnit: Joi.string().max(50).required(),
|
||||||
applicationType: Joi.string().valid('liquid', 'granular').optional()
|
applicationType: Joi.string().valid('liquid', 'granular').optional()
|
||||||
})).min(1).required()
|
})).min(1).required()
|
||||||
});
|
}).or('lawnSectionId', 'lawnSectionIds'); // At least one lawn section parameter is required
|
||||||
|
|
||||||
const applicationLogSchema = Joi.object({
|
const applicationLogSchema = Joi.object({
|
||||||
planId: Joi.number().integer().positive(),
|
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) => {
|
onSubmit={async (planData) => {
|
||||||
try {
|
try {
|
||||||
if (editingPlan) {
|
if (editingPlan) {
|
||||||
// Edit existing plan - handle multiple areas
|
// Edit existing plan - backend now supports multiple areas natively
|
||||||
if (planData.selectedAreas.length === 1) {
|
const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId));
|
||||||
// Single area - update existing plan normally
|
const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null;
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(planPromises);
|
// Calculate total area for all selected areas
|
||||||
toast.success(`Created ${planData.selectedAreas.length} application plan(s) successfully`);
|
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);
|
setShowPlanForm(false);
|
||||||
@@ -720,7 +608,7 @@ const ApplicationPlanModal = ({
|
|||||||
|
|
||||||
setPlanData({
|
setPlanData({
|
||||||
propertyId: propertyId?.toString() || '',
|
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 || '',
|
productId: selectedProduct?.uniqueId || '',
|
||||||
selectedProduct: selectedProduct,
|
selectedProduct: selectedProduct,
|
||||||
selectedProducts: [],
|
selectedProducts: [],
|
||||||
@@ -749,7 +637,7 @@ const ApplicationPlanModal = ({
|
|||||||
|
|
||||||
setPlanData({
|
setPlanData({
|
||||||
propertyId: propertyId?.toString() || '',
|
propertyId: propertyId?.toString() || '',
|
||||||
selectedAreas: [editingPlan.section?.id].filter(Boolean), // Allow adding more areas
|
selectedAreas: editingPlan.sections?.map(s => s.id) || [], // Handle multiple areas
|
||||||
productId: '',
|
productId: '',
|
||||||
selectedProduct: null,
|
selectedProduct: null,
|
||||||
selectedProducts: selectedProducts,
|
selectedProducts: selectedProducts,
|
||||||
|
|||||||
Reference in New Issue
Block a user