This commit is contained in:
Jake Kasper
2025-09-02 09:32:25 -05:00
parent f29876b041
commit ae3275299e
2 changed files with 109 additions and 36 deletions

View File

@@ -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

View File

@@ -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;