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(),
|
||||
|
||||
Reference in New Issue
Block a user