asdfasdf
This commit is contained in:
@@ -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); }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
if (plan) {
|
||||||
|
const rs = await wateringAPI.getPlanPoints(plan.id);
|
||||||
|
setPoints(rs.data?.data?.points || []);
|
||||||
|
} else {
|
||||||
setPoints(prev=> prev.filter(p=> p.id!==selectedPointId));
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user