diff --git a/frontend/src/components/Applications/ApplicationExecutionModal.js b/frontend/src/components/Applications/ApplicationExecutionModal.js new file mode 100644 index 0000000..330258e --- /dev/null +++ b/frontend/src/components/Applications/ApplicationExecutionModal.js @@ -0,0 +1,337 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { applicationsAPI } 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 [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 = 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 = 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 = async () => { + 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 + })) || [] + }; + + try { + // Save the application log to the backend + await applicationsAPI.createLog(logData); + toast.success('Application completed successfully'); + onComplete(); + } catch (error) { + console.error('Failed to save application log:', error); + toast.error('Failed to save application log'); + } + }; + + // 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 + useEffect(() => { + return () => { + if (watchId) { + navigator.geolocation.clearWatch(watchId); + } + }; + }, [watchId]); + + return ( +
+ Target: {targetSpeed.toFixed(1)} mph • Current: {currentSpeed.toFixed(1)} mph +
+