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 { 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
@@ -502,4 +510,4 @@ const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onCo
); );
}; };
export default ApplicationExecutionModal; export default ApplicationExecutionModal;

View File

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