From 0d23bd0dc027ad1775020ab3f481a11577622e85 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Fri, 5 Sep 2025 08:22:34 -0400 Subject: [PATCH] watering --- backend/src/routes/watering.js | 59 +++++++++++++++ frontend/src/pages/Watering/Watering.js | 96 +++++++++++++++++++++++-- frontend/src/services/api.js | 2 + 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/watering.js b/backend/src/routes/watering.js index 1248de3..1d29563 100644 --- a/backend/src/routes/watering.js +++ b/backend/src/routes/watering.js @@ -101,4 +101,63 @@ router.post('/plans/:id/points', async (req,res,next)=>{ } catch (e) { next(e); } }); +// PUT /api/watering/points/:id - update a single point +router.put('/points/:id', async (req, res, next) => { + try { + const pointId = req.params.id; + // Verify ownership by joining plans + const own = await pool.query( + `SELECT wpp.id FROM watering_plan_points wpp + JOIN watering_plans wp ON wpp.plan_id = wp.id + WHERE wpp.id=$1 AND wp.user_id=$2`, [pointId, req.user.id] + ); + if (own.rows.length === 0) throw new AppError('Point not found', 404); + + const p = req.body || {}; + // Recompute coverage if geometry changed + const coverage = computeCoverageSqft({ + sprinkler_head_type: p.sprinklerHeadType, + sprinkler_throw_feet: p.throwFeet, + sprinkler_degrees: p.degrees, + sprinkler_length_feet: p.lengthFeet, + sprinkler_width_feet: p.widthFeet + }); + + const rs = await pool.query( + `UPDATE watering_plan_points SET + lat = COALESCE($1, lat), + lng = COALESCE($2, lng), + duration_minutes = COALESCE($3, duration_minutes), + sprinkler_mount = COALESCE($4, sprinkler_mount), + sprinkler_head_type = COALESCE($5, sprinkler_head_type), + sprinkler_gpm = COALESCE($6, sprinkler_gpm), + sprinkler_throw_feet = COALESCE($7, sprinkler_throw_feet), + sprinkler_degrees = COALESCE($8, sprinkler_degrees), + 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 *`, + [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] + ); + res.json({ success:true, data:{ point: rs.rows[0] }}); + } catch (e) { next(e); } +}); + +// DELETE /api/watering/points/:id +router.delete('/points/:id', async (req,res,next)=>{ + try { + const pointId = req.params.id; + const own = await pool.query( + `SELECT wpp.id FROM watering_plan_points wpp + JOIN watering_plans wp ON wpp.plan_id = wp.id + WHERE wpp.id=$1 AND wp.user_id=$2`, [pointId, req.user.id] + ); + if (own.rows.length === 0) throw new AppError('Point not found', 404); + await pool.query('DELETE FROM watering_plan_points WHERE id=$1', [pointId]); + res.json({ success:true }); + } catch (e) { next(e); } +}); + module.exports = router; diff --git a/frontend/src/pages/Watering/Watering.js b/frontend/src/pages/Watering/Watering.js index a4d017b..f777b7d 100644 --- a/frontend/src/pages/Watering/Watering.js +++ b/frontend/src/pages/Watering/Watering.js @@ -45,6 +45,10 @@ const Watering = () => { const [watchId, setWatchId] = useState(null); const [sprinklers, setSprinklers] = useState([]); const [selectedSprinklerId, setSelectedSprinklerId] = useState(''); + const [selectedPointId, setSelectedPointId] = useState(null); + const selectedPoint = useMemo(()=> points.find(p=> p.id===selectedPointId), [points, selectedPointId]); + const [editForm, setEditForm] = useState(null); + const [satellite, setSatellite] = useState(false); useEffect(() => { (async () => { try { @@ -255,10 +259,57 @@ const Watering = () => {
Plan Points
+ {selectedPoint && editForm && ( +
+
Edit Selected
+ {(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? ( + <> + + setEditForm({...editForm, degrees: parseInt(e.target.value||'0',10)})} /> + + setEditForm({...editForm, headingDegrees: parseInt(e.target.value||'0',10)%360})} /> + + setEditForm({...editForm, throwFeet: parseFloat(e.target.value||'0')})} /> + + ) : ( + <> + + setEditForm({...editForm, lengthFeet: parseFloat(e.target.value||'0')})} /> + + setEditForm({...editForm, widthFeet: parseFloat(e.target.value||'0')})} /> + + setEditForm({...editForm, headingDegrees: parseInt(e.target.value||'0',10)%360})} /> + + )} + + setEditForm({...editForm, durationMinutes: parseInt(e.target.value||'0',10)})} /> +
+ + +
+
+ )}
@@ -267,7 +318,10 @@ const Watering = () => {
{guiding && currentPos && points[guideIndex] ? ( <>Go to point #{points[guideIndex].sequence}: {distanceFeet(currentPos, {lat: Number(points[guideIndex].lat), lng: Number(points[guideIndex].lng)}).toFixed(0)} ft away ) : 'Map'}
-
+
+ {!guiding ? ( ) : ( @@ -281,7 +335,11 @@ const Watering = () => {
- + {satellite ? ( + + ) : ( + + )} {sections.map((s)=> ( ))} @@ -296,7 +354,15 @@ const Watering = () => { }); return ( - + { + const ll = e.target.getLatLng(); + updatePointField(pt.id, { lat: ll.lat, lng: ll.lng }); + }, + click: ()=> onSelectPoint(pt) + }} + /> {cov?.kind==='circle' && ( cov.degrees && cov.degrees < 360 ? ( @@ -328,3 +394,25 @@ const Watering = () => { }; export default Watering; + const onSelectPoint = (pt) => { + setSelectedPointId(pt.id); + setEditForm({ + durationMinutes: pt.duration_minutes || 0, + mountType: pt.sprinkler_mount, + sprinklerHeadType: pt.sprinkler_head_type, + gpm: Number(pt.sprinkler_gpm||0), + throwFeet: Number(pt.sprinkler_throw_feet||0), + degrees: Number(pt.sprinkler_degrees||360), + lengthFeet: Number(pt.sprinkler_length_feet||0), + widthFeet: Number(pt.sprinkler_width_feet||0), + headingDegrees: Number(pt.sprinkler_heading_degrees||0) + }); + }; + + const updatePointField = async (id, patch) => { + try { + const r = await wateringAPI.updatePoint(id, patch); + const np = r.data?.data?.point; + setPoints(prev => prev.map(p=> p.id===id? np: p)); + } catch(e){ toast.error('Failed to update point'); } + }; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 897d83d..9eb920c 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -231,6 +231,8 @@ export const wateringAPI = { createPlan: (payload) => apiClient.post('/watering/plans', payload), getPlanPoints: (planId) => apiClient.get(`/watering/plans/${planId}/points`), addPlanPoint: (planId, payload) => apiClient.post(`/watering/plans/${planId}/points`, payload), + updatePoint: (pointId, payload) => apiClient.put(`/watering/points/${pointId}`, payload), + deletePoint: (pointId) => apiClient.delete(`/watering/points/${pointId}`), }; // Admin API endpoints