From 3160c6c5811c30e53960ce740bac5561e98d1438 Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Wed, 27 Aug 2025 09:30:03 -0400 Subject: [PATCH] application tracking --- .../Applications/ApplicationExecutionModal.js | 337 ++++++++++++++++++ .../src/pages/Applications/Applications.js | 39 +- 2 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Applications/ApplicationExecutionModal.js 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 ( +
+
+
+

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 ? ( + + ) : ( + <> + + + + )} + +
+
+
+ ); +}; + +export default ApplicationExecutionModal; \ No newline at end of file diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index ca6e899..1ce03b4 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -12,6 +12,7 @@ import { import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import PropertyMap from '../../components/Maps/PropertyMap'; +import ApplicationExecutionModal from '../../components/Applications/ApplicationExecutionModal'; import toast from 'react-hot-toast'; const Applications = () => { @@ -28,6 +29,7 @@ const Applications = () => { const [spreaderRecommendation, setSpreaderRecommendation] = useState(null); const [loadingRecommendation, setLoadingRecommendation] = useState(false); const [executingApplication, setExecutingApplication] = useState(null); + const [showExecutionModal, setShowExecutionModal] = useState(false); useEffect(() => { @@ -132,9 +134,21 @@ const Applications = () => { } }; - const handleExecuteApplication = (application) => { - setExecutingApplication(application); - alert(`Executing application for ${application.propertyName} - ${application.sectionNames}`); + 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) { @@ -1385,6 +1399,25 @@ const ApplicationPlanModal = ({ )} + + {/* Application Execution Modal */} + {showExecutionModal && executingApplication && ( + { + setShowExecutionModal(false); + setExecutingApplication(null); + }} + onComplete={() => { + // Refresh applications list + fetchApplications(); + // Close modal + setShowExecutionModal(false); + setExecutingApplication(null); + }} + /> + )} ); };