diff --git a/frontend/src/pages/Watering/Watering.js b/frontend/src/pages/Watering/Watering.js index edecd45..8d9df0b 100644 --- a/frontend/src/pages/Watering/Watering.js +++ b/frontend/src/pages/Watering/Watering.js @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, useMapEvents, useMap } from 'react-leaflet'; +import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, Popup, useMapEvents, useMap } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api'; import toast from 'react-hot-toast'; @@ -50,6 +50,62 @@ const Watering = () => { const [editForm, setEditForm] = useState(null); const [satellite, setSatellite] = useState(false); + // Select a point helper (for popup+state sync) + 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) + }); + }; + + // Optimistic local patch + save to server + const applyLocalPatch = (id, patch) => { + setPoints(prev => prev.map(p => p.id === id ? { ...p, + sprinkler_degrees: patch.degrees !== undefined ? patch.degrees : p.sprinkler_degrees, + sprinkler_heading_degrees: patch.headingDegrees !== undefined ? patch.headingDegrees : p.sprinkler_heading_degrees, + sprinkler_throw_feet: patch.throwFeet !== undefined ? patch.throwFeet : p.sprinkler_throw_feet, + sprinkler_length_feet: patch.lengthFeet !== undefined ? patch.lengthFeet : p.sprinkler_length_feet, + sprinkler_width_feet: patch.widthFeet !== undefined ? patch.widthFeet : p.sprinkler_width_feet, + duration_minutes: patch.durationMinutes !== undefined ? patch.durationMinutes : p.duration_minutes, + lat: patch.lat !== undefined ? patch.lat : p.lat, + lng: patch.lng !== undefined ? patch.lng : p.lng + } : p)); + }; + + const updatePointField = async (id, patch) => { + try { + applyLocalPatch(id, patch); + 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'); } + }; + + const nudgePointLocation = (pt, dxFeet, dyFeet) => { + const lat = Number(pt.lat), lng = Number(pt.lng); + const Rlat = 111320; // meters per degree lat + const Rlng = Math.cos(lat*Math.PI/180)*111320; // meters per degree lng + const dxm = dxFeet*0.3048; // east-west + const dym = dyFeet*0.3048; // north-south + const newLat = lat + (dym / Rlat); + const newLng = lng + (dxm / Rlng); + updatePointField(pt.id, { lat: newLat, lng: newLng }); + }; + + const adjustHeading = (pt, delta) => { + const cur = Number(pt.sprinkler_heading_degrees || 0); + let next = (cur + delta) % 360; if (next < 0) next += 360; + updatePointField(pt.id, { headingDegrees: next }); + }; + useEffect(() => { (async () => { try { const [pr, eq] = await Promise.all([propertiesAPI.getAll(), equipmentAPI.getAll()]); @@ -388,6 +444,33 @@ const Watering = () => { click: ()=> onSelectPoint(pt) }} /> + +
+
Adjust Sprinkler
+
+ Heading: + + + {Number(pt.sprinkler_heading_degrees||0)}° + + +
+
+
+ +
+ +
Move 1 ft
+ +
+ +
+
+
+ +
+
+
{cov?.kind==='circle' && ( cov.degrees && cov.degrees < 360 ? (