diff --git a/backend/src/routes/watering.js b/backend/src/routes/watering.js index 1d29563..f765494 100644 --- a/backend/src/routes/watering.js +++ b/backend/src/routes/watering.js @@ -60,6 +60,28 @@ router.post('/plans', async (req, res, next) => { } catch (e) { next(e); } }); +// PUT /api/watering/plans/:id - rename/update notes +router.put('/plans/:id', async (req, res, next) => { + try { + const planId = req.params.id; + // Verify ownership + 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); + + const { name, notes } = req.body || {}; + const rs = await pool.query( + `UPDATE watering_plans + SET name = COALESCE($1, name), + notes = COALESCE($2, notes), + updated_at = NOW() + WHERE id=$3 + RETURNING *`, + [name, notes, planId] + ); + res.json({ success: true, data: { plan: rs.rows[0] } }); + } catch (e) { next(e); } +}); + // GET /api/watering/plans/:id/points router.get('/plans/:id/points', async (req, res, next) => { try { diff --git a/frontend/src/pages/Watering/Watering.js b/frontend/src/pages/Watering/Watering.js index 4aac85d..c60a6c7 100644 --- a/frontend/src/pages/Watering/Watering.js +++ b/frontend/src/pages/Watering/Watering.js @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, Popup, useMapEvents, useMap } from 'react-leaflet'; +import { Icon } from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api'; import toast from 'react-hot-toast'; @@ -38,6 +39,7 @@ const Watering = () => { durationMinutes: 60 }); const [plan, setPlan] = useState(null); + const [plans, setPlans] = useState([]); const [points, setPoints] = useState([]); const [guiding, setGuiding] = useState(false); const [guideIndex, setGuideIndex] = useState(0); @@ -49,6 +51,26 @@ const Watering = () => { const selectedPoint = useMemo(()=> points.find(p=> p.id===selectedPointId), [points, selectedPointId]); const [editForm, setEditForm] = useState(null); const [satellite, setSatellite] = useState(false); + const [rename, setRename] = useState(''); + + // Distinct colors for points + const palette = ['#ef4444','#f59e0b','#10b981','#3b82f6','#8b5cf6','#ec4899','#14b8a6','#84cc16','#f97316','#06b6d4','#a855f7','#22c55e']; + const pointColor = (pt) => { + const idx = points.findIndex(p=> p.id===pt.id); + return palette[(idx>=0? idx:0) % palette.length]; + }; + const iconCache = useMemo(()=> ({}), []); + const markerIcon = (color) => { + if (iconCache[color]) return iconCache[color]; + const svg = `\n \n \n `; + const ic = new Icon({ + iconUrl: 'data:image/svg+xml;base64,' + btoa(svg), + iconSize: [16,16], + iconAnchor: [8,8] + }); + iconCache[color] = ic; + return ic; + }; // Select a point helper (for popup+state sync) const onSelectPoint = (pt) => { @@ -83,6 +105,17 @@ const Watering = () => { const updatePointField = async (id, patch) => { try { applyLocalPatch(id, patch); + if (id === selectedPointId) { + setEditForm(prev => prev ? { + ...prev, + degrees: patch.degrees !== undefined ? patch.degrees : prev.degrees, + headingDegrees: patch.headingDegrees !== undefined ? patch.headingDegrees : prev.headingDegrees, + throwFeet: patch.throwFeet !== undefined ? patch.throwFeet : prev.throwFeet, + lengthFeet: patch.lengthFeet !== undefined ? patch.lengthFeet : prev.lengthFeet, + widthFeet: patch.widthFeet !== undefined ? patch.widthFeet : prev.widthFeet, + durationMinutes: patch.durationMinutes !== undefined ? patch.durationMinutes : prev.durationMinutes, + } : prev); + } const r = await wateringAPI.updatePoint(id, patch); const np = r.data?.data?.point; setPoints(prev => prev.map(p=> p.id===id? np: p)); @@ -121,6 +154,19 @@ const Watering = () => { const r = await propertiesAPI.getById(pid); const p = r.data?.data?.property; setSelectedProperty(p); const secs = (p.sections||[]).map(s => ({ ...s, polygonData: typeof s.polygonData === 'string' ? JSON.parse(s.polygonData) : s.polygonData })); setSections(secs); + // Load existing watering plans for this property + const pl = await wateringAPI.getPlans({ property_id: pid }); + const plansList = pl.data?.data?.plans || []; + setPlans(plansList); + if (plansList.length) { + const sel = plansList[0]; + setPlan(sel); + const pts = await wateringAPI.getPlanPoints(sel.id); + setPoints(pts.data?.data?.points || []); + } else { + setPlan(null); + setPoints([]); + } } catch(e){ toast.error('Failed to load property'); } }; @@ -128,10 +174,32 @@ const Watering = () => { const ensurePlan = async () => { if (plan) return plan; if (!selectedProperty) { toast.error('Select a property first'); return null; } - try { const r = await wateringAPI.createPlan({ propertyId: selectedProperty.id, name: `${selectedProperty.name} - Sprinklers` }); setPlan(r.data?.data?.plan); return r.data?.data?.plan; } + try { + const r = await wateringAPI.createPlan({ propertyId: selectedProperty.id, name: `${selectedProperty.name} - Sprinklers` }); + const newPlan = r.data?.data?.plan; + setPlan(newPlan); + // refresh list and clear points + const pl = await wateringAPI.getPlans({ property_id: selectedProperty.id }); + setPlans(pl.data?.data?.plans || []); + setPoints([]); + return newPlan; + } catch(e){ toast.error('Failed to create plan'); return null; } }; + const selectPlan = async (planId) => { + const p = (plans||[]).find(pl=> pl.id === parseInt(planId)); + setPlan(p || null); + if (p) { + try { + const rs = await wateringAPI.getPlanPoints(p.id); + setPoints(rs.data?.data?.points || []); + } catch(e){ toast.error('Failed to load plan points'); setPoints([]); } + } else { + setPoints([]); + } + }; + const onPlace = async (latlng) => { const p = await ensurePlan(); if (!p) return; try { @@ -178,6 +246,10 @@ const Watering = () => { return null; }; + useEffect(() => { + setRename(plan?.name || ''); + }, [plan]); + const startGuidance = () => { if (points.length === 0) { toast.error('Add at least one point'); return; } if (!navigator.geolocation) { toast.error('GPS not available'); return; } @@ -246,6 +318,69 @@ const Watering = () => {

Watering - Sprinklers

+
+
Plan Points
+
    + {points.map(pt => ( +
  • onSelectPoint(pt)}> + + + #{pt.sequence} + {Number(pt.lat).toFixed(5)}, {Number(pt.lng).toFixed(5)} + + + {(pt.sprinkler_head_type||'-').replace('_',' ')} + {pt.sprinkler_throw_feet ? ` • ${Number(pt.sprinkler_throw_feet)}ft` : ''} + {pt.sprinkler_degrees ? ` • ${pt.sprinkler_degrees}°` : ''} + +
  • + ))} + {points.length===0 &&
  • No points yet
  • } +
+ {selectedPoint && editForm && ( +
+
Selected Details
+
+ Type: {editForm.sprinklerHeadType || '-'} • GPM: {editForm.gpm || '-'} • Duration: {editForm.durationMinutes||0}m +
+ {(editForm.sprinklerHeadType==='rotor_impact' || editForm.sprinklerHeadType==='spray_fixed') ? ( +
Throw: {editForm.throwFeet||0} ft • Degrees: {editForm.degrees||0}° • Heading: {editForm.headingDegrees||0}°
+ ) : ( +
Length: {editForm.lengthFeet||0} ft • Width: {editForm.widthFeet||0} ft • Heading: {editForm.headingDegrees||0}°
+ )} +
+ )} +
+
+
Plan
+
+ + +
+ {plan && ( +
+ setRename(e.target.value)} /> + +
+ )} +