This commit is contained in:
Jake Kasper
2025-09-05 11:11:53 -04:00
parent 7789eadb61
commit c517b28f51
2 changed files with 61 additions and 9 deletions

View File

@@ -184,13 +184,27 @@ router.put('/points/:id', async (req, res, next) => {
router.delete('/points/:id', async (req,res,next)=>{ router.delete('/points/:id', async (req,res,next)=>{
try { try {
const pointId = req.params.id; const pointId = req.params.id;
const own = await pool.query( // Verify and fetch plan id for resequencing
`SELECT wpp.id FROM watering_plan_points wpp const chk = await pool.query(
`SELECT wpp.plan_id FROM watering_plan_points wpp
JOIN watering_plans wp ON wpp.plan_id = wp.id JOIN watering_plans wp ON wpp.plan_id = wp.id
WHERE wpp.id=$1 AND wp.user_id=$2`, [pointId, req.user.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); if (chk.rows.length === 0) throw new AppError('Point not found', 404);
const planId = chk.rows[0].plan_id;
await pool.query('DELETE FROM watering_plan_points WHERE id=$1', [pointId]); await pool.query('DELETE FROM watering_plan_points WHERE id=$1', [pointId]);
// Resequence remaining points for the plan
await pool.query(
`WITH ordered AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY sequence, id) AS rn
FROM watering_plan_points WHERE plan_id=$1
)
UPDATE watering_plan_points w
SET sequence = o.rn
FROM ordered o
WHERE w.id = o.id`,
[planId]
);
res.json({ success:true }); res.json({ success:true });
} catch (e) { next(e); } } catch (e) { next(e); }
}); });

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, Popup, useMapEvents, useMap } from 'react-leaflet'; import { MapContainer, TileLayer, Polygon, Marker, Circle, Rectangle, Popup, useMapEvents, useMap, Polyline } from 'react-leaflet';
import { Icon } from 'leaflet'; import { Icon } from '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';
@@ -271,6 +271,15 @@ const Watering = () => {
return (2*R*Math.atan2(Math.sqrt(A),Math.sqrt(1-A)))*3.28084; return (2*R*Math.atan2(Math.sqrt(A),Math.sqrt(1-A)))*3.28084;
}; };
const bearingDegrees = (a,b) => {
const toRad = d=> d*Math.PI/180, toDeg = r=> (r*180/Math.PI+360)%360;
const lat1 = toRad(a.lat), lat2 = toRad(b.lat);
const dLng = toRad(b.lng - a.lng);
const y = Math.sin(dLng) * Math.cos(lat2);
const x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLng);
return Math.round(toDeg(Math.atan2(y,x)));
};
// Build sector polygon (approx) for Leaflet // Build sector polygon (approx) for Leaflet
const sectorPolygon = (center, radiusFeet, startDeg, endDeg, steps=60) => { const sectorPolygon = (center, radiusFeet, startDeg, endDeg, steps=60) => {
const [clat, clng] = [Number(center.lat), Number(center.lng)]; const [clat, clng] = [Number(center.lat), Number(center.lng)];
@@ -560,7 +569,12 @@ const Watering = () => {
}}>Save</button> }}>Save</button>
<button className="btn-secondary" onClick={async ()=>{ <button className="btn-secondary" onClick={async ()=>{
await wateringAPI.deletePoint(selectedPointId); await wateringAPI.deletePoint(selectedPointId);
setPoints(prev=> prev.filter(p=> p.id!==selectedPointId)); if (plan) {
const rs = await wateringAPI.getPlanPoints(plan.id);
setPoints(rs.data?.data?.points || []);
} else {
setPoints(prev=> prev.filter(p=> p.id!==selectedPointId));
}
setSelectedPointId(null); setEditForm(null); setSelectedPointId(null); setEditForm(null);
}}>Delete</button> }}>Delete</button>
</div> </div>
@@ -572,9 +586,15 @@ const Watering = () => {
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<div className="card p-0" style={{height:'70vh', position:'relative'}}> <div className="card p-0" style={{height:'70vh', position:'relative'}}>
<div className="flex items-center justify-between p-3 border-b"> <div className="flex items-center justify-between p-3 border-b">
<div className="text-sm text-gray-700">{guiding && currentPos && points[guideIndex] ? ( <div className="text-sm text-gray-700">{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</> const tgt = {lat:Number(points[guideIndex].lat), lng:Number(points[guideIndex].lng)};
) : 'Map'}</div> const dist = distanceFeet(currentPos, tgt);
const brg = bearingDegrees(currentPos, tgt);
if (dist <= 15) {
return <span className="text-green-700 font-semibold">Arrived at #{points[guideIndex].sequence} {dist.toFixed(0)} ft</span>;
}
return <> {brg}° {dist.toFixed(0)} ft Point #{points[guideIndex].sequence}</>;
})() : 'Map'}</div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<label className="text-xs flex items-center gap-1"> <label className="text-xs flex items-center gap-1">
<input type="checkbox" checked={satellite} onChange={(e)=> setSatellite(e.target.checked)} /> Satellite <input type="checkbox" checked={satellite} onChange={(e)=> setSatellite(e.target.checked)} /> Satellite
@@ -601,6 +621,16 @@ const Watering = () => {
<Polygon key={s.id} positions={s.polygonData?.coordinates?.[0]||[]} pathOptions={{ color:'#16a34a', weight:2, fillOpacity:0.1 }} /> <Polygon key={s.id} positions={s.polygonData?.coordinates?.[0]||[]} pathOptions={{ color:'#16a34a', weight:2, fillOpacity:0.1 }} />
))} ))}
{placing && <SprinklerPlacement onPlace={onPlace} />} {placing && <SprinklerPlacement onPlace={onPlace} />}
{guiding && currentPos && points[guideIndex] && (
<>
{/* Current location marker */}
<Marker position={[currentPos.lat, currentPos.lng]} icon={markerIcon('#0ea5e9')} />
{/* Line to target */}
<Polyline positions={[[currentPos.lat, currentPos.lng], [Number(points[guideIndex].lat), Number(points[guideIndex].lng)]]} pathOptions={{ color:'#0ea5e9', dashArray:'6 6' }} />
{/* Arrival ring */}
<Circle center={[Number(points[guideIndex].lat), Number(points[guideIndex].lng)]} radius={15*0.3048} pathOptions={{ color:'#22c55e', fillOpacity:0.15, weight:2 }} />
</>
)}
{/* Legend overlay */} {/* Legend overlay */}
{points.length > 0 && ( {points.length > 0 && (
<div style={{ position:'absolute', right:10, bottom:10, zIndex:1000 }}> <div style={{ position:'absolute', right:10, bottom:10, zIndex:1000 }}>
@@ -703,7 +733,15 @@ const Watering = () => {
<div></div> <div></div>
</div> </div>
<div className="flex gap-2 pt-1"> <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> <button className="btn-secondary" onClick={async ()=>{
await wateringAPI.deletePoint(pt.id);
if (plan) {
const rs = await wateringAPI.getPlanPoints(plan.id);
setPoints(rs.data?.data?.points || []);
} else {
setPoints(prev=> prev.filter(p=> p.id!==pt.id));
}
}}>Delete</button>
</div> </div>
</div> </div>
</Popup> </Popup>