diff --git a/backend/src/routes/watering.js b/backend/src/routes/watering.js index f765494..ec198af 100644 --- a/backend/src/routes/watering.js +++ b/backend/src/routes/watering.js @@ -102,6 +102,15 @@ router.post('/plans/:id/points', async (req,res,next)=>{ const seqrs = await pool.query('SELECT COALESCE(MAX(sequence),0)+1 as next FROM watering_plan_points WHERE plan_id=$1',[planId]); const sequence = seqrs.rows[0].next; const payload = req.body || {}; + let equipment_id = null, equipment_name = null; + if (payload.equipmentId) { + // verify ownership, otherwise ignore silently + const eq = await pool.query('SELECT id, custom_name, manufacturer, model FROM user_equipment WHERE id=$1 AND user_id=$2', [payload.equipmentId, req.user.id]); + if (eq.rows.length) { + equipment_id = eq.rows[0].id; + equipment_name = payload.equipmentName || eq.rows[0].custom_name || `${eq.rows[0].manufacturer||''} ${eq.rows[0].model||''}`.trim() || null; + } + } const point = { sprinkler_head_type: payload.sprinklerHeadType, sprinkler_throw_feet: payload.throwFeet, @@ -113,11 +122,13 @@ router.post('/plans/:id/points', async (req,res,next)=>{ const ins = await pool.query( `INSERT INTO watering_plan_points (plan_id, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type, - sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft, sprinkler_heading_degrees) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, + sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft, sprinkler_heading_degrees, + equipment_id, equipment_name) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) RETURNING *`, [planId, sequence, payload.lat, payload.lng, payload.durationMinutes||0, payload.mountType||null, payload.sprinklerHeadType||null, payload.gpm||null, payload.throwFeet||null, payload.degrees||null, - payload.lengthFeet||null, payload.widthFeet||null, coverage, payload.headingDegrees||null] + payload.lengthFeet||null, payload.widthFeet||null, coverage, payload.headingDegrees||null, + equipment_id, equipment_name] ); res.status(201).json({ success:true, data:{ point: ins.rows[0] }}); } catch (e) { next(e); } @@ -158,10 +169,12 @@ router.put('/points/:id', async (req, res, next) => { sprinkler_length_feet = COALESCE($9, sprinkler_length_feet), sprinkler_width_feet = COALESCE($10, sprinkler_width_feet), sprinkler_heading_degrees = COALESCE($11, sprinkler_heading_degrees), - coverage_sqft = COALESCE($12, coverage_sqft) - WHERE id=$13 RETURNING *`, + coverage_sqft = COALESCE($12, coverage_sqft), + equipment_id = COALESCE($13, equipment_id), + equipment_name = COALESCE($14, equipment_name) + WHERE id=$15 RETURNING *`, [p.lat, p.lng, p.durationMinutes, p.mountType, p.sprinklerHeadType, p.gpm, - p.throwFeet, p.degrees, p.lengthFeet, p.widthFeet, p.headingDegrees, coverage || null, pointId] + p.throwFeet, p.degrees, p.lengthFeet, p.widthFeet, p.headingDegrees, coverage || null, p.equipmentId || null, p.equipmentName || null, pointId] ); res.json({ success:true, data:{ point: rs.rows[0] }}); } catch (e) { next(e); } @@ -182,4 +195,44 @@ router.delete('/points/:id', async (req,res,next)=>{ } catch (e) { next(e); } }); +// DELETE /api/watering/plans/:id +router.delete('/plans/:id', async (req, res, next) => { + try { + const planId = req.params.id; + const own = await pool.query('SELECT id FROM watering_plans WHERE id=$1 AND user_id=$2', [planId, req.user.id]); + if (own.rows.length === 0) throw new AppError('Plan not found', 404); + await pool.query('DELETE FROM watering_plans WHERE id=$1', [planId]); + res.json({ success: true }); + } catch (e) { next(e); } +}); + module.exports = router; +// Duplicate a watering plan with all points +router.post('/plans/:id/duplicate', async (req, res, next) => { + try { + const planId = req.params.id; + const own = await pool.query('SELECT * FROM watering_plans WHERE id=$1 AND user_id=$2', [planId, req.user.id]); + if (own.rows.length === 0) throw new AppError('Plan not found', 404); + const src = own.rows[0]; + const newName = (req.body && req.body.name) || `${src.name} (Copy ${new Date().toLocaleDateString()})`; + const ins = await pool.query( + `INSERT INTO watering_plans(user_id, property_id, name, notes) + VALUES ($1,$2,$3,$4) RETURNING *`, + [req.user.id, src.property_id, newName, src.notes] + ); + const newPlan = ins.rows[0]; + // Copy points + await pool.query( + `INSERT INTO watering_plan_points + (plan_id, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type, + sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, + coverage_sqft, sprinkler_heading_degrees, equipment_id, equipment_name) + SELECT $1, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type, + sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, + coverage_sqft, sprinkler_heading_degrees, equipment_id, equipment_name + FROM watering_plan_points WHERE plan_id=$2`, + [newPlan.id, planId] + ); + res.status(201).json({ success: true, data: { plan: newPlan } }); + } catch (e) { next(e); } +}); diff --git a/database/migrations/V13__watering_point_equipment.sql b/database/migrations/V13__watering_point_equipment.sql new file mode 100644 index 0000000..b55a06b --- /dev/null +++ b/database/migrations/V13__watering_point_equipment.sql @@ -0,0 +1,7 @@ +-- Link watering plan points to saved equipment and store name snapshot +ALTER TABLE watering_plan_points + ADD COLUMN IF NOT EXISTS equipment_id INTEGER REFERENCES user_equipment(id) ON DELETE SET NULL, + ADD COLUMN IF NOT EXISTS equipment_name VARCHAR(255); + +SELECT 'Added equipment_id and equipment_name to watering_plan_points' as migration_status; + diff --git a/frontend/src/pages/Watering/Watering.js b/frontend/src/pages/Watering/Watering.js index c60a6c7..eb0c42d 100644 --- a/frontend/src/pages/Watering/Watering.js +++ b/frontend/src/pages/Watering/Watering.js @@ -222,7 +222,9 @@ const Watering = () => { degrees: base.degrees, lengthFeet: base.lengthFeet, widthFeet: base.widthFeet, - headingDegrees: sprinklerForm.headingDegrees || 0 + headingDegrees: sprinklerForm.headingDegrees || 0, + equipmentId: eq ? eq.id : null, + equipmentName: eq ? (eq.customName || `${eq.manufacturer||''} ${eq.model||''}`.trim()) : null }; const r = await wateringAPI.addPlanPoint(p.id, payload); setPoints(prev => [...prev, r.data?.data?.point]); @@ -331,7 +333,7 @@ const Watering = () => { {Number(pt.lat).toFixed(5)}, {Number(pt.lng).toFixed(5)} - {(pt.sprinkler_head_type||'-').replace('_',' ')} + {pt.equipment_name || (pt.sprinkler_head_type||'-').replace('_',' ')} {pt.sprinkler_throw_feet ? ` • ${Number(pt.sprinkler_throw_feet)}ft` : ''} {pt.sprinkler_degrees ? ` • ${pt.sprinkler_degrees}°` : ''} @@ -343,6 +345,9 @@ const Watering = () => {