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 PropertyMap from '../Maps/PropertyMap';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onComplete }) => {
|
||||
const [isTracking, setIsTracking] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [gpsTrack, setGpsTrack] = useState([]);
|
||||
const [currentLocation, setCurrentLocation] = useState(null);
|
||||
const [currentSpeed, setCurrentSpeed] = useState(0);
|
||||
@@ -14,6 +15,8 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
const [previousTime, setPreviousTime] = useState(null);
|
||||
const [totalDistance, setTotalDistance] = useState(0);
|
||||
const [averageSpeed, setAverageSpeed] = useState(0);
|
||||
const totalDistanceRef = useRef(0);
|
||||
const wakeLockRef = useRef(null);
|
||||
const [sections, setSections] = useState([]);
|
||||
const [mapCenter, setMapCenter] = useState(null);
|
||||
const [planDetails, setPlanDetails] = useState(null);
|
||||
@@ -124,6 +127,11 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
}, [currentSpeed, targetSpeed]);
|
||||
|
||||
// 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 = () => {
|
||||
if (!navigator.geolocation) {
|
||||
toast.error('GPS not available on this device');
|
||||
@@ -131,9 +139,9 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
}
|
||||
|
||||
setIsTracking(true);
|
||||
setStartTime(new Date());
|
||||
setGpsTrack([]);
|
||||
setTotalDistance(0);
|
||||
setIsPaused(false);
|
||||
if (!startTime) setStartTime(new Date());
|
||||
requestWakeLock();
|
||||
setPreviousLocation(null);
|
||||
setPreviousTime(null);
|
||||
|
||||
@@ -163,15 +171,13 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
if (timeDiff > 0) {
|
||||
const speedMph = (distance / timeDiff) * 2.237; // Convert m/s to mph
|
||||
setCurrentSpeed(speedMph);
|
||||
|
||||
setTotalDistance(prev => prev + distance);
|
||||
const newTotal = totalDistanceRef.current + distance;
|
||||
totalDistanceRef.current = newTotal;
|
||||
setTotalDistance(newTotal);
|
||||
|
||||
// Update average speed
|
||||
const totalTime = (timestamp - startTime) / 1000; // seconds
|
||||
if (totalTime > 0) {
|
||||
const avgSpeedMph = (totalDistance + distance) / totalTime * 2.237;
|
||||
setAverageSpeed(avgSpeedMph);
|
||||
}
|
||||
if (totalTime > 0) setAverageSpeed((newTotal / totalTime) * 2.237);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +207,8 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
setWatchId(null);
|
||||
}
|
||||
setIsTracking(false);
|
||||
setIsPaused(true);
|
||||
releaseWakeLock();
|
||||
};
|
||||
|
||||
// Complete application
|
||||
@@ -305,6 +313,12 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
const response = await applicationsAPI.createLog(logData);
|
||||
console.log('CreateLog response:', response);
|
||||
toast.success('Application completed successfully');
|
||||
// reset local tracking state
|
||||
setGpsTrack([]);
|
||||
setTotalDistance(0);
|
||||
totalDistanceRef.current = 0;
|
||||
setAverageSpeed(0);
|
||||
setIsPaused(false);
|
||||
onComplete();
|
||||
} catch (error) {
|
||||
console.error('Failed to save application log:', error);
|
||||
@@ -333,9 +347,8 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (watchId) {
|
||||
navigator.geolocation.clearWatch(watchId);
|
||||
}
|
||||
if (watchId) navigator.geolocation.clearWatch(watchId);
|
||||
releaseWakeLock();
|
||||
};
|
||||
}, [watchId]);
|
||||
|
||||
@@ -468,12 +481,7 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3">
|
||||
{!isTracking ? (
|
||||
<button
|
||||
onClick={startTracking}
|
||||
className="btn-primary flex-1"
|
||||
>
|
||||
Start Application
|
||||
</button>
|
||||
<button onClick={startTracking} className="btn-primary flex-1">{isPaused ? 'Resume' : 'Start'} Application</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
@@ -502,4 +510,4 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationExecutionModal;
|
||||
export default ApplicationExecutionModal;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { propertiesAPI, equipmentAPI } from '../../services/api';
|
||||
import { applicationsAPI } from '../../services/api';
|
||||
import { weatherAPI } from '../../services/api';
|
||||
import { apiClient } from '../../services/api';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { propertiesAPI, equipmentAPI, apiClient } from '../../services/api';
|
||||
import * as turf from '@turf/turf';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const directionOptions = [
|
||||
@@ -24,11 +22,14 @@ const Mowing = () => {
|
||||
const [cutHeight, setCutHeight] = useState(3.0);
|
||||
const [direction, setDirection] = useState('N_S');
|
||||
const [tracking, setTracking] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [gpsTrack, setGpsTrack] = useState([]);
|
||||
const [startTime, setStartTime] = useState(null);
|
||||
const [totalDistance, setTotalDistance] = useState(0);
|
||||
const [averageSpeed, setAverageSpeed] = useState(0);
|
||||
const [totalDistance, setTotalDistance] = useState(0); // meters
|
||||
const totalDistanceRef = useRef(0);
|
||||
const [averageSpeed, setAverageSpeed] = useState(0); // mph
|
||||
const [watchId, setWatchId] = useState(null);
|
||||
const wakeLockRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
@@ -71,25 +72,45 @@ const Mowing = () => {
|
||||
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 = () => {
|
||||
if (!selectedProperty || selectedSections.length === 0 || !selectedEquipment) {
|
||||
toast.error('Select property, sections and mower first');
|
||||
return;
|
||||
}
|
||||
if (!navigator.geolocation) { toast.error('GPS not available'); return; }
|
||||
requestWakeLock();
|
||||
setTracking(true);
|
||||
setStartTime(new Date());
|
||||
setGpsTrack([]);
|
||||
setTotalDistance(0);
|
||||
setIsPaused(false);
|
||||
// Resume if we already have a track
|
||||
if (!startTime) setStartTime(new Date());
|
||||
const id = navigator.geolocation.watchPosition((pos) => {
|
||||
const { latitude, longitude, speed } = pos.coords;
|
||||
const point = { lat: latitude, lng: longitude, timestamp: new Date(pos.timestamp).toISOString(), speed: speed || 0 };
|
||||
setGpsTrack((prev) => {
|
||||
if (prev.length > 0) {
|
||||
const meters = haversineMeters(prev[prev.length-1], point);
|
||||
setTotalDistance((d) => d + meters);
|
||||
const seconds = (new Date(pos.timestamp) - startTime) / 1000;
|
||||
if (seconds > 0) setAverageSpeed((( (d + meters) / seconds) * 2.237));
|
||||
const newTotal = totalDistanceRef.current + meters;
|
||||
totalDistanceRef.current = newTotal;
|
||||
setTotalDistance(newTotal);
|
||||
const seconds = startTime ? ((new Date(pos.timestamp) - startTime) / 1000) : 0;
|
||||
if (seconds > 0) setAverageSpeed((newTotal / seconds) * 2.237);
|
||||
}
|
||||
return [...prev, point];
|
||||
});
|
||||
@@ -103,12 +124,44 @@ const Mowing = () => {
|
||||
if (watchId) navigator.geolocation.clearWatch(watchId);
|
||||
setWatchId(null);
|
||||
setTracking(false);
|
||||
setIsPaused(true);
|
||||
releaseWakeLock();
|
||||
};
|
||||
|
||||
const complete = async () => {
|
||||
stop();
|
||||
try {
|
||||
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 = {
|
||||
propertyId: Number(selectedProperty),
|
||||
lawnSectionIds: selectedSections.map(Number),
|
||||
@@ -119,18 +172,26 @@ const Mowing = () => {
|
||||
averageSpeed: Math.max(averageSpeed, 0.1),
|
||||
durationSeconds,
|
||||
totalDistanceMeters: Math.round(totalDistance*100)/100,
|
||||
areaCoveredSqft,
|
||||
notes: ''
|
||||
};
|
||||
const resp = await apiClient.post('/mowing/sessions', payload);
|
||||
toast.success('Mowing session saved');
|
||||
// reset
|
||||
setGpsTrack([]); setStartTime(null); setTotalDistance(0); setAverageSpeed(0);
|
||||
setGpsTrack([]); setStartTime(null); setTotalDistance(0); totalDistanceRef.current = 0; setAverageSpeed(0); setIsPaused(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error(e.response?.data?.message || 'Failed to save session');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => { // cleanup on unmount
|
||||
if (watchId) navigator.geolocation.clearWatch(watchId);
|
||||
releaseWakeLock();
|
||||
};
|
||||
}, [watchId]);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<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="flex gap-3">
|
||||
{!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>
|
||||
@@ -190,4 +256,3 @@ const Mowing = () => {
|
||||
};
|
||||
|
||||
export default Mowing;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user