mowing
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import { applicationsAPI, propertiesAPI } from '../../services/api';
|
import { applicationsAPI, propertiesAPI } from '../../services/api';
|
||||||
import PropertyMap from '../Maps/PropertyMap';
|
import PropertyMap from '../Maps/PropertyMap';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onComplete }) => {
|
const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onComplete }) => {
|
||||||
const [isTracking, setIsTracking] = useState(false);
|
const [isTracking, setIsTracking] = useState(false);
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [gpsTrack, setGpsTrack] = useState([]);
|
const [gpsTrack, setGpsTrack] = useState([]);
|
||||||
const [currentLocation, setCurrentLocation] = useState(null);
|
const [currentLocation, setCurrentLocation] = useState(null);
|
||||||
const [currentSpeed, setCurrentSpeed] = useState(0);
|
const [currentSpeed, setCurrentSpeed] = useState(0);
|
||||||
@@ -14,6 +15,8 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
const [previousTime, setPreviousTime] = useState(null);
|
const [previousTime, setPreviousTime] = useState(null);
|
||||||
const [totalDistance, setTotalDistance] = useState(0);
|
const [totalDistance, setTotalDistance] = useState(0);
|
||||||
const [averageSpeed, setAverageSpeed] = useState(0);
|
const [averageSpeed, setAverageSpeed] = useState(0);
|
||||||
|
const totalDistanceRef = useRef(0);
|
||||||
|
const wakeLockRef = useRef(null);
|
||||||
const [sections, setSections] = useState([]);
|
const [sections, setSections] = useState([]);
|
||||||
const [mapCenter, setMapCenter] = useState(null);
|
const [mapCenter, setMapCenter] = useState(null);
|
||||||
const [planDetails, setPlanDetails] = useState(null);
|
const [planDetails, setPlanDetails] = useState(null);
|
||||||
@@ -124,6 +127,11 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
}, [currentSpeed, targetSpeed]);
|
}, [currentSpeed, targetSpeed]);
|
||||||
|
|
||||||
// Start GPS tracking
|
// Start GPS tracking
|
||||||
|
const requestWakeLock = async () => {
|
||||||
|
try { if ('wakeLock' in navigator) { wakeLockRef.current = await navigator.wakeLock.request('screen'); } } catch {}
|
||||||
|
};
|
||||||
|
const releaseWakeLock = async () => { try { await wakeLockRef.current?.release(); } catch {} wakeLockRef.current = null; };
|
||||||
|
|
||||||
const startTracking = () => {
|
const startTracking = () => {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
toast.error('GPS not available on this device');
|
toast.error('GPS not available on this device');
|
||||||
@@ -131,9 +139,9 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsTracking(true);
|
setIsTracking(true);
|
||||||
setStartTime(new Date());
|
setIsPaused(false);
|
||||||
setGpsTrack([]);
|
if (!startTime) setStartTime(new Date());
|
||||||
setTotalDistance(0);
|
requestWakeLock();
|
||||||
setPreviousLocation(null);
|
setPreviousLocation(null);
|
||||||
setPreviousTime(null);
|
setPreviousTime(null);
|
||||||
|
|
||||||
@@ -163,15 +171,13 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
if (timeDiff > 0) {
|
if (timeDiff > 0) {
|
||||||
const speedMph = (distance / timeDiff) * 2.237; // Convert m/s to mph
|
const speedMph = (distance / timeDiff) * 2.237; // Convert m/s to mph
|
||||||
setCurrentSpeed(speedMph);
|
setCurrentSpeed(speedMph);
|
||||||
|
const newTotal = totalDistanceRef.current + distance;
|
||||||
setTotalDistance(prev => prev + distance);
|
totalDistanceRef.current = newTotal;
|
||||||
|
setTotalDistance(newTotal);
|
||||||
|
|
||||||
// Update average speed
|
// Update average speed
|
||||||
const totalTime = (timestamp - startTime) / 1000; // seconds
|
const totalTime = (timestamp - startTime) / 1000; // seconds
|
||||||
if (totalTime > 0) {
|
if (totalTime > 0) setAverageSpeed((newTotal / totalTime) * 2.237);
|
||||||
const avgSpeedMph = (totalDistance + distance) / totalTime * 2.237;
|
|
||||||
setAverageSpeed(avgSpeedMph);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +207,8 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
setWatchId(null);
|
setWatchId(null);
|
||||||
}
|
}
|
||||||
setIsTracking(false);
|
setIsTracking(false);
|
||||||
|
setIsPaused(true);
|
||||||
|
releaseWakeLock();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Complete application
|
// Complete application
|
||||||
@@ -305,6 +313,12 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
const response = await applicationsAPI.createLog(logData);
|
const response = await applicationsAPI.createLog(logData);
|
||||||
console.log('CreateLog response:', response);
|
console.log('CreateLog response:', response);
|
||||||
toast.success('Application completed successfully');
|
toast.success('Application completed successfully');
|
||||||
|
// reset local tracking state
|
||||||
|
setGpsTrack([]);
|
||||||
|
setTotalDistance(0);
|
||||||
|
totalDistanceRef.current = 0;
|
||||||
|
setAverageSpeed(0);
|
||||||
|
setIsPaused(false);
|
||||||
onComplete();
|
onComplete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save application log:', error);
|
console.error('Failed to save application log:', error);
|
||||||
@@ -333,9 +347,8 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
// Cleanup on unmount
|
// Cleanup on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (watchId) {
|
if (watchId) navigator.geolocation.clearWatch(watchId);
|
||||||
navigator.geolocation.clearWatch(watchId);
|
releaseWakeLock();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [watchId]);
|
}, [watchId]);
|
||||||
|
|
||||||
@@ -468,12 +481,7 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
|||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{!isTracking ? (
|
{!isTracking ? (
|
||||||
<button
|
<button onClick={startTracking} className="btn-primary flex-1">{isPaused ? 'Resume' : 'Start'} Application</button>
|
||||||
onClick={startTracking}
|
|
||||||
className="btn-primary flex-1"
|
|
||||||
>
|
|
||||||
Start Application
|
|
||||||
</button>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { propertiesAPI, equipmentAPI } from '../../services/api';
|
import { propertiesAPI, equipmentAPI, apiClient } from '../../services/api';
|
||||||
import { applicationsAPI } from '../../services/api';
|
import * as turf from '@turf/turf';
|
||||||
import { weatherAPI } from '../../services/api';
|
|
||||||
import { apiClient } from '../../services/api';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const directionOptions = [
|
const directionOptions = [
|
||||||
@@ -24,11 +22,14 @@ const Mowing = () => {
|
|||||||
const [cutHeight, setCutHeight] = useState(3.0);
|
const [cutHeight, setCutHeight] = useState(3.0);
|
||||||
const [direction, setDirection] = useState('N_S');
|
const [direction, setDirection] = useState('N_S');
|
||||||
const [tracking, setTracking] = useState(false);
|
const [tracking, setTracking] = useState(false);
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const [gpsTrack, setGpsTrack] = useState([]);
|
const [gpsTrack, setGpsTrack] = useState([]);
|
||||||
const [startTime, setStartTime] = useState(null);
|
const [startTime, setStartTime] = useState(null);
|
||||||
const [totalDistance, setTotalDistance] = useState(0);
|
const [totalDistance, setTotalDistance] = useState(0); // meters
|
||||||
const [averageSpeed, setAverageSpeed] = useState(0);
|
const totalDistanceRef = useRef(0);
|
||||||
|
const [averageSpeed, setAverageSpeed] = useState(0); // mph
|
||||||
const [watchId, setWatchId] = useState(null);
|
const [watchId, setWatchId] = useState(null);
|
||||||
|
const wakeLockRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -71,25 +72,45 @@ const Mowing = () => {
|
|||||||
return 2 * R * Math.atan2(Math.sqrt(A), Math.sqrt(1-A));
|
return 2 * R * Math.atan2(Math.sqrt(A), Math.sqrt(1-A));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requestWakeLock = async () => {
|
||||||
|
try {
|
||||||
|
if ('wakeLock' in navigator) {
|
||||||
|
wakeLockRef.current = await navigator.wakeLock.request('screen');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if unsupported
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const releaseWakeLock = async () => {
|
||||||
|
try {
|
||||||
|
await wakeLockRef.current?.release();
|
||||||
|
} catch {}
|
||||||
|
wakeLockRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
const start = () => {
|
const start = () => {
|
||||||
if (!selectedProperty || selectedSections.length === 0 || !selectedEquipment) {
|
if (!selectedProperty || selectedSections.length === 0 || !selectedEquipment) {
|
||||||
toast.error('Select property, sections and mower first');
|
toast.error('Select property, sections and mower first');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!navigator.geolocation) { toast.error('GPS not available'); return; }
|
if (!navigator.geolocation) { toast.error('GPS not available'); return; }
|
||||||
|
requestWakeLock();
|
||||||
setTracking(true);
|
setTracking(true);
|
||||||
setStartTime(new Date());
|
setIsPaused(false);
|
||||||
setGpsTrack([]);
|
// Resume if we already have a track
|
||||||
setTotalDistance(0);
|
if (!startTime) setStartTime(new Date());
|
||||||
const id = navigator.geolocation.watchPosition((pos) => {
|
const id = navigator.geolocation.watchPosition((pos) => {
|
||||||
const { latitude, longitude, speed } = pos.coords;
|
const { latitude, longitude, speed } = pos.coords;
|
||||||
const point = { lat: latitude, lng: longitude, timestamp: new Date(pos.timestamp).toISOString(), speed: speed || 0 };
|
const point = { lat: latitude, lng: longitude, timestamp: new Date(pos.timestamp).toISOString(), speed: speed || 0 };
|
||||||
setGpsTrack((prev) => {
|
setGpsTrack((prev) => {
|
||||||
if (prev.length > 0) {
|
if (prev.length > 0) {
|
||||||
const meters = haversineMeters(prev[prev.length-1], point);
|
const meters = haversineMeters(prev[prev.length-1], point);
|
||||||
setTotalDistance((d) => d + meters);
|
const newTotal = totalDistanceRef.current + meters;
|
||||||
const seconds = (new Date(pos.timestamp) - startTime) / 1000;
|
totalDistanceRef.current = newTotal;
|
||||||
if (seconds > 0) setAverageSpeed((( (d + meters) / seconds) * 2.237));
|
setTotalDistance(newTotal);
|
||||||
|
const seconds = startTime ? ((new Date(pos.timestamp) - startTime) / 1000) : 0;
|
||||||
|
if (seconds > 0) setAverageSpeed((newTotal / seconds) * 2.237);
|
||||||
}
|
}
|
||||||
return [...prev, point];
|
return [...prev, point];
|
||||||
});
|
});
|
||||||
@@ -103,12 +124,44 @@ const Mowing = () => {
|
|||||||
if (watchId) navigator.geolocation.clearWatch(watchId);
|
if (watchId) navigator.geolocation.clearWatch(watchId);
|
||||||
setWatchId(null);
|
setWatchId(null);
|
||||||
setTracking(false);
|
setTracking(false);
|
||||||
|
setIsPaused(true);
|
||||||
|
releaseWakeLock();
|
||||||
};
|
};
|
||||||
|
|
||||||
const complete = async () => {
|
const complete = async () => {
|
||||||
stop();
|
stop();
|
||||||
try {
|
try {
|
||||||
const durationSeconds = startTime ? Math.round((new Date() - startTime)/1000) : 0;
|
const durationSeconds = startTime ? Math.round((new Date() - startTime)/1000) : 0;
|
||||||
|
// Compute coverage using mower deck width if available
|
||||||
|
const mower = equipment.find(e => String(e.id) === String(selectedEquipment));
|
||||||
|
const widthInches = mower?.cuttingWidthInches || mower?.workingWidthInches || null;
|
||||||
|
let areaCoveredSqft = null;
|
||||||
|
if (widthInches && gpsTrack.length > 1 && sections.length > 0) {
|
||||||
|
try {
|
||||||
|
const line = turf.lineString(gpsTrack.map(p => [p.lng, p.lat]));
|
||||||
|
const bufferKm = (widthInches / 12 / 2) * 0.3048 / 1000; // inches -> ft -> meters -> km
|
||||||
|
const swath = turf.buffer(line, bufferKm, { units: 'kilometers' });
|
||||||
|
const plannedPolys = sections.map(s => {
|
||||||
|
let poly = s.polygonData;
|
||||||
|
if (typeof poly === 'string') { try { poly = JSON.parse(poly);} catch { poly = null; } }
|
||||||
|
if (!poly?.coordinates?.[0]) return null;
|
||||||
|
const coords = poly.coordinates[0].map(([lat,lng]) => [lng,lat]);
|
||||||
|
return turf.polygon([coords]);
|
||||||
|
}).filter(Boolean);
|
||||||
|
if (plannedPolys.length) {
|
||||||
|
const plannedUnion = plannedPolys.reduce((acc,cur)=> acc? turf.union(acc,cur):cur, null);
|
||||||
|
if (plannedUnion) {
|
||||||
|
const overlap = turf.intersect(swath, plannedUnion);
|
||||||
|
if (overlap) {
|
||||||
|
const sqm = turf.area(overlap);
|
||||||
|
areaCoveredSqft = Math.round((sqm / 0.092903));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore coverage calculation errors
|
||||||
|
}
|
||||||
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
propertyId: Number(selectedProperty),
|
propertyId: Number(selectedProperty),
|
||||||
lawnSectionIds: selectedSections.map(Number),
|
lawnSectionIds: selectedSections.map(Number),
|
||||||
@@ -119,18 +172,26 @@ const Mowing = () => {
|
|||||||
averageSpeed: Math.max(averageSpeed, 0.1),
|
averageSpeed: Math.max(averageSpeed, 0.1),
|
||||||
durationSeconds,
|
durationSeconds,
|
||||||
totalDistanceMeters: Math.round(totalDistance*100)/100,
|
totalDistanceMeters: Math.round(totalDistance*100)/100,
|
||||||
|
areaCoveredSqft,
|
||||||
notes: ''
|
notes: ''
|
||||||
};
|
};
|
||||||
const resp = await apiClient.post('/mowing/sessions', payload);
|
const resp = await apiClient.post('/mowing/sessions', payload);
|
||||||
toast.success('Mowing session saved');
|
toast.success('Mowing session saved');
|
||||||
// reset
|
// reset
|
||||||
setGpsTrack([]); setStartTime(null); setTotalDistance(0); setAverageSpeed(0);
|
setGpsTrack([]); setStartTime(null); setTotalDistance(0); totalDistanceRef.current = 0; setAverageSpeed(0); setIsPaused(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.error(e.response?.data?.message || 'Failed to save session');
|
toast.error(e.response?.data?.message || 'Failed to save session');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => { // cleanup on unmount
|
||||||
|
if (watchId) navigator.geolocation.clearWatch(watchId);
|
||||||
|
releaseWakeLock();
|
||||||
|
};
|
||||||
|
}, [watchId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Mowing Tracker</h1>
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">Mowing Tracker</h1>
|
||||||
@@ -170,7 +231,12 @@ const Mowing = () => {
|
|||||||
<div className="bg-white p-4 rounded shadow mb-4">
|
<div className="bg-white p-4 rounded shadow mb-4">
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
{!tracking ? (
|
{!tracking ? (
|
||||||
<button className="btn-primary" onClick={start}>Start Tracking</button>
|
<>
|
||||||
|
<button className="btn-primary" onClick={start}>{isPaused ? 'Resume' : 'Start'} Tracking</button>
|
||||||
|
{gpsTrack.length > 0 && (
|
||||||
|
<button className="btn-secondary" onClick={() => { setGpsTrack([]); setStartTime(null); setTotalDistance(0); totalDistanceRef.current = 0; setAverageSpeed(0); setIsPaused(false); }}>Restart</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button className="btn-secondary" onClick={stop}>Pause</button>
|
<button className="btn-secondary" onClick={stop}>Pause</button>
|
||||||
@@ -190,4 +256,3 @@ const Mowing = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default Mowing;
|
export default Mowing;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user