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
{points.map(pt => (
- - #{pt.sequence}: {Number(pt.lat).toFixed(5)}, {Number(pt.lng).toFixed(5)} • {pt.duration_minutes} min
+ - onSelectPoint(pt)}>
+ #{pt.sequence}: {Number(pt.lat).toFixed(5)}, {Number(pt.lng).toFixed(5)} • {pt.duration_minutes} min
+
))}
{points.length===0 && - No points yet
}
+ {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