+
+ GPS Tracking Active
+
+
+ Track Points: {gpsTrack.length}
+
+ {currentLocation && (
+
+ Accuracy: ±{Math.round(currentLocation.accuracy || 0)}m
+
+ )}
+
+ )}
+
{/* Section stats */}
- {sections.length > 0 && !isDrawing && (
+ {sections.length > 0 && !isDrawing && mode !== "execution" && (
{sections.length} Section{sections.length !== 1 ? 's' : ''}
diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js
index ca7e39c..c944fe3 100644
--- a/frontend/src/pages/Applications/Applications.js
+++ b/frontend/src/pages/Applications/Applications.js
@@ -6,7 +6,8 @@ import {
WrenchScrewdriverIcon,
CalculatorIcon,
PencilIcon,
- TrashIcon
+ TrashIcon,
+ PlayIcon
} from '@heroicons/react/24/outline';
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
import LoadingSpinner from '../../components/UI/LoadingSpinner';
@@ -26,6 +27,14 @@ const Applications = () => {
const [propertyCache, setPropertyCache] = useState({});
const [spreaderRecommendation, setSpreaderRecommendation] = useState(null);
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
+
+ // Application execution state
+ const [executingApplication, setExecutingApplication] = useState(null);
+ const [showExecutionModal, setShowExecutionModal] = useState(false);
+ const [isTracking, setIsTracking] = useState(false);
+ const [gpsTrack, setGpsTrack] = useState([]);
+ const [currentSpeed, setCurrentSpeed] = useState(0);
+ const [currentLocation, setCurrentLocation] = useState(null);
useEffect(() => {
fetchApplications();
@@ -129,6 +138,23 @@ const Applications = () => {
}
};
+ const handleExecuteApplication = async (application) => {
+ try {
+ // Set the executing application and show the modal
+ setExecutingApplication(application);
+
+ // Also fetch the property details if we don't have them
+ if (!selectedPropertyDetails || selectedPropertyDetails.id !== application.property?.id) {
+ await fetchPropertyDetails(application.property?.id || application.section?.propertyId);
+ }
+
+ setShowExecutionModal(true);
+ } catch (error) {
+ console.error('Failed to start application execution:', error);
+ toast.error('Failed to start application execution');
+ }
+ };
+
if (loading) {
return (
@@ -364,6 +390,15 @@ const Applications = () => {
Created {new Date(application.createdAt).toLocaleDateString()}
+ {application.status === 'planned' && (
+
+ )}
)}
+
+ {/* Application Execution Modal */}
+ {showExecutionModal && executingApplication && (
+
{
+ setShowExecutionModal(false);
+ setExecutingApplication(null);
+ setIsTracking(false);
+ setGpsTrack([]);
+ setCurrentLocation(null);
+ setCurrentSpeed(0);
+ }}
+ onComplete={async (logData) => {
+ try {
+ // Save the application log to the backend
+ await applicationsAPI.createLog(logData);
+ toast.success('Application completed successfully');
+
+ // Refresh applications list
+ fetchApplications();
+
+ // Close modal
+ setShowExecutionModal(false);
+ setExecutingApplication(null);
+ setIsTracking(false);
+ setGpsTrack([]);
+ setCurrentLocation(null);
+ setCurrentSpeed(0);
+ } catch (error) {
+ console.error('Failed to save application log:', error);
+ toast.error('Failed to save application log');
+ }
+ }}
+ />
+ )}
+
+
+ );
+};
+
+// Application Execution Modal Component
+const ApplicationExecutionModal = ({ application, propertyDetails, onClose, onComplete }) => {
+ const [isTracking, setIsTracking] = useState(false);
+ const [gpsTrack, setGpsTrack] = useState([]);
+ const [currentLocation, setCurrentLocation] = useState(null);
+ const [currentSpeed, setCurrentSpeed] = useState(0);
+ const [startTime, setStartTime] = useState(null);
+ const [watchId, setWatchId] = useState(null);
+ const [previousLocation, setPreviousLocation] = useState(null);
+ const [previousTime, setPreviousTime] = useState(null);
+ const [totalDistance, setTotalDistance] = useState(0);
+ const [averageSpeed, setAverageSpeed] = useState(0);
+
+ // Calculate target speed for liquid applications
+ const targetSpeed = React.useMemo(() => {
+ if (!application.products || application.products.length === 0) return null;
+
+ // Find the first liquid product to get equipment specs
+ const firstProduct = application.products[0];
+ if (firstProduct.applicationType !== 'liquid') return null;
+
+ // This would be calculated based on equipment specs and application rate
+ // For now, return a default target speed - this should be calculated from the plan
+ return 3.0; // 3.0 mph default target
+ }, [application]);
+
+ const speedStatus = React.useMemo(() => {
+ if (!targetSpeed || !currentSpeed) return 'normal';
+
+ const tolerance = 0.1; // 10% tolerance
+ const lowerBound = targetSpeed * (1 - tolerance);
+ const upperBound = targetSpeed * (1 + tolerance);
+
+ if (currentSpeed < lowerBound) return 'slow';
+ if (currentSpeed > upperBound) return 'fast';
+ return 'normal';
+ }, [currentSpeed, targetSpeed]);
+
+ // Start GPS tracking
+ const startTracking = () => {
+ if (!navigator.geolocation) {
+ toast.error('GPS not available on this device');
+ return;
+ }
+
+ setIsTracking(true);
+ setStartTime(new Date());
+ setGpsTrack([]);
+ setTotalDistance(0);
+ setPreviousLocation(null);
+ setPreviousTime(null);
+
+ const watchId = navigator.geolocation.watchPosition(
+ (position) => {
+ const { latitude, longitude, accuracy, speed } = position.coords;
+ const timestamp = new Date(position.timestamp);
+
+ const newLocation = {
+ lat: latitude,
+ lng: longitude,
+ accuracy,
+ timestamp: timestamp.toISOString(),
+ speed: speed || 0
+ };
+
+ setCurrentLocation(newLocation);
+
+ // Calculate speed if we have a previous location
+ if (previousLocation && previousTime) {
+ const distance = calculateDistance(
+ previousLocation.lat, previousLocation.lng,
+ latitude, longitude
+ );
+ const timeDiff = (timestamp - previousTime) / 1000; // seconds
+
+ if (timeDiff > 0) {
+ const speedMph = (distance / timeDiff) * 2.237; // Convert m/s to mph
+ setCurrentSpeed(speedMph);
+
+ setTotalDistance(prev => prev + distance);
+
+ // Update average speed
+ const totalTime = (timestamp - startTime) / 1000; // seconds
+ if (totalTime > 0) {
+ const avgSpeedMph = (totalDistance + distance) / totalTime * 2.237;
+ setAverageSpeed(avgSpeedMph);
+ }
+ }
+ }
+
+ setPreviousLocation({ lat: latitude, lng: longitude });
+ setPreviousTime(timestamp);
+
+ setGpsTrack(prev => [...prev, newLocation]);
+ },
+ (error) => {
+ console.error('GPS error:', error);
+ toast.error(`GPS error: ${error.message}`);
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 5000,
+ maximumAge: 1000
+ }
+ );
+
+ setWatchId(watchId);
+ };
+
+ // Stop GPS tracking
+ const stopTracking = () => {
+ if (watchId) {
+ navigator.geolocation.clearWatch(watchId);
+ setWatchId(null);
+ }
+ setIsTracking(false);
+ };
+
+ // Complete application
+ const completeApplication = () => {
+ stopTracking();
+
+ const endTime = new Date();
+ const duration = startTime ? (endTime - startTime) / 1000 : 0; // seconds
+
+ const logData = {
+ planId: application.id,
+ lawnSectionId: application.sections?.[0]?.id || application.section?.id, // Use first section for multi-area plans
+ equipmentId: application.equipment?.id,
+ applicationDate: endTime.toISOString(),
+ gpsTrack: gpsTrack,
+ averageSpeed: averageSpeed,
+ areaCovered: application.totalSectionArea || application.sectionArea || 0,
+ notes: `Application completed via mobile tracking. Duration: ${Math.round(duration/60)} minutes`,
+ products: application.products?.map(product => ({
+ productId: product.productId,
+ userProductId: product.userProductId,
+ rateAmount: product.rateAmount,
+ rateUnit: product.rateUnit,
+ actualProductAmount: product.actualProductAmount || product.productAmount,
+ actualWaterAmount: product.actualWaterAmount || product.waterAmount,
+ actualSpeedMph: averageSpeed
+ })) || []
+ };
+
+ onComplete(logData);
+ };
+
+ // Calculate distance between two points in meters
+ const calculateDistance = (lat1, lng1, lat2, lng2) => {
+ const R = 6371e3; // Earth's radius in meters
+ const φ1 = lat1 * Math.PI/180;
+ const φ2 = lat2 * Math.PI/180;
+ const Δφ = (lat2-lat1) * Math.PI/180;
+ const Δλ = (lng2-lng1) * Math.PI/180;
+
+ const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
+ Math.cos(φ1) * Math.cos(φ2) *
+ Math.sin(Δλ/2) * Math.sin(Δλ/2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
+
+ return R * c;
+ };
+
+ // Cleanup on unmount
+ React.useEffect(() => {
+ return () => {
+ if (watchId) {
+ navigator.geolocation.clearWatch(watchId);
+ }
+ };
+ }, [watchId]);
+
+ return (
+
+
+
+
Execute Application
+
+
+
+ {/* Application Details */}
+
+
Application Details
+
+
+ Property: {application.propertyName}
+
+
+ Areas: {application.sectionNames}
+
+
+ Equipment: {application.equipmentName}
+
+
+ Area: {Math.round(application.totalSectionArea || application.sectionArea || 0).toLocaleString()} sq ft
+
+
+
+ {/* Products */}
+
+
Products to Apply:
+
+ {application.products?.map((product, index) => (
+
+
{product.productName}
+
+ Rate: {product.rateAmount} {product.rateUnit}
+ {product.productAmount && ` • Product: ${product.productAmount}`}
+ {product.waterAmount && ` • Water: ${product.waterAmount}`}
+
+
+ ))}
+
+
+
+
+ {/* Speed Guidance for Liquid Applications */}
+ {targetSpeed && (
+
+
+
+
Speed Guidance
+
+ Target: {targetSpeed.toFixed(1)} mph • Current: {currentSpeed.toFixed(1)} mph
+
+
+
+ {speedStatus === 'normal' ? '✓ Good Speed' :
+ speedStatus === 'slow' ? '↑ Go Faster' :
+ '↓ Slow Down'}
+
+
+
+ )}
+
+ {/* Map with GPS Track */}
+
+
Application Area & GPS Track
+
+ {propertyDetails && (
+
s.id) || []}
+ mode="execution"
+ gpsTrack={gpsTrack}
+ currentLocation={currentLocation}
+ />
+ )}
+
+
+
+ {/* Tracking Stats */}
+ {isTracking && (
+
+
Tracking Statistics
+
+
+ Duration:
+ {startTime ? Math.round((new Date() - startTime) / 60000) : 0} min
+
+
+ Distance:
+ {(totalDistance * 3.28084).toFixed(0)} ft
+
+
+ Avg Speed:
+ {averageSpeed.toFixed(1)} mph
+
+
+ Track Points:
+ {gpsTrack.length}
+
+
+
+ )}
+
+ {/* Action Buttons */}
+
+ {!isTracking ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
);