This commit is contained in:
Jake Kasper
2025-09-05 08:39:03 -04:00
parent ff029fe073
commit 99c5c63167

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react'; 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 'leaflet/dist/leaflet.css';
import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api'; import { propertiesAPI, wateringAPI, equipmentAPI } from '../../services/api';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@@ -50,6 +50,62 @@ const Watering = () => {
const [editForm, setEditForm] = useState(null); const [editForm, setEditForm] = useState(null);
const [satellite, setSatellite] = useState(false); 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 () => { useEffect(() => { (async () => {
try { try {
const [pr, eq] = await Promise.all([propertiesAPI.getAll(), equipmentAPI.getAll()]); const [pr, eq] = await Promise.all([propertiesAPI.getAll(), equipmentAPI.getAll()]);
@@ -388,6 +444,33 @@ const Watering = () => {
click: ()=> onSelectPoint(pt) click: ()=> onSelectPoint(pt)
}} }}
/> />
<Popup>
<div className="space-y-2 text-sm">
<div className="font-medium">Adjust Sprinkler</div>
<div className="flex items-center gap-2">
<span className="text-xs">Heading:</span>
<button className="px-2 py-1 border rounded" onClick={()=> adjustHeading(pt, -10)}>-10°</button>
<button className="px-2 py-1 border rounded" onClick={()=> adjustHeading(pt, -1)}>-1°</button>
<span className="px-2">{Number(pt.sprinkler_heading_degrees||0)}°</span>
<button className="px-2 py-1 border rounded" onClick={()=> adjustHeading(pt, +1)}>+1°</button>
<button className="px-2 py-1 border rounded" onClick={()=> adjustHeading(pt, +10)}>+10°</button>
</div>
<div className="grid grid-cols-3 gap-2 items-center">
<div></div>
<button className="px-2 py-1 border rounded" onClick={()=> nudgePointLocation(pt, 0, +1)}></button>
<div></div>
<button className="px-2 py-1 border rounded" onClick={()=> nudgePointLocation(pt, -1, 0)}></button>
<div className="text-center text-xs">Move 1 ft</div>
<button className="px-2 py-1 border rounded" onClick={()=> nudgePointLocation(pt, +1, 0)}></button>
<div></div>
<button className="px-2 py-1 border rounded" onClick={()=> nudgePointLocation(pt, 0, -1)}></button>
<div></div>
</div>
<div className="flex gap-2 pt-1">
<button className="btn-secondary" onClick={async ()=>{ await wateringAPI.deletePoint(pt.id); setPoints(prev=> prev.filter(p=> p.id!==pt.id)); }}>Delete</button>
</div>
</div>
</Popup>
{cov?.kind==='circle' && ( {cov?.kind==='circle' && (
cov.degrees && cov.degrees < 360 ? ( cov.degrees && cov.degrees < 360 ? (
<Polygon positions={sectorPolygon({lat:Number(pt.lat),lng:Number(pt.lng)}, cov.radius, (pt.sprinkler_heading_degrees||0), (pt.sprinkler_heading_degrees||0)+cov.degrees)} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} /> <Polygon positions={sectorPolygon({lat:Number(pt.lat),lng:Number(pt.lng)}, cov.radius, (pt.sprinkler_heading_degrees||0), (pt.sprinkler_heading_degrees||0)+cov.degrees)} pathOptions={{ color:'#2563eb', fillOpacity:0.2 }} />