asdf
This commit is contained in:
@@ -27,10 +27,8 @@ const Applications = () => {
|
|||||||
const [propertyCache, setPropertyCache] = useState({});
|
const [propertyCache, setPropertyCache] = useState({});
|
||||||
const [spreaderRecommendation, setSpreaderRecommendation] = useState(null);
|
const [spreaderRecommendation, setSpreaderRecommendation] = useState(null);
|
||||||
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
|
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
|
||||||
|
|
||||||
// Application execution state
|
|
||||||
const [executingApplication, setExecutingApplication] = useState(null);
|
const [executingApplication, setExecutingApplication] = useState(null);
|
||||||
const [showExecutionModal, setShowExecutionModal] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
fetchApplications();
|
||||||
@@ -134,21 +132,9 @@ const Applications = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExecuteApplication = async (application) => {
|
const handleExecuteApplication = (application) => {
|
||||||
try {
|
setExecutingApplication(application);
|
||||||
// Set the executing application and show the modal
|
alert(`Executing application for ${application.propertyName} - ${application.sectionNames}`);
|
||||||
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) {
|
if (loading) {
|
||||||
@@ -270,10 +256,6 @@ const Applications = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedPropertyDetails(null); // Clear previous property data
|
setSelectedPropertyDetails(null); // Clear previous property data
|
||||||
setShowPlanForm(true);
|
setShowPlanForm(true);
|
||||||
// Test the execution state variables
|
|
||||||
console.log('Test:', showExecutionModal, executingApplication);
|
|
||||||
setShowExecutionModal(false);
|
|
||||||
setExecutingApplication(null);
|
|
||||||
}}
|
}}
|
||||||
className="btn-primary flex items-center gap-2"
|
className="btn-primary flex items-center gap-2"
|
||||||
>
|
>
|
||||||
@@ -1402,337 +1384,9 @@ const ApplicationPlanModal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Application Execution Modal */}
|
|
||||||
{showExecutionModal && (
|
|
||||||
<div>Test: {executingApplication?.id}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Application Execution Modal Component
|
export default Applications;
|
||||||
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 (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h3 className="text-xl font-semibold">Execute Application</h3>
|
|
||||||
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Application Details */}
|
|
||||||
<div className="bg-gray-50 p-4 rounded-lg mb-6">
|
|
||||||
<h4 className="font-medium mb-2">Application Details</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Property:</span> {application.propertyName}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Areas:</span> {application.sectionNames}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Equipment:</span> {application.equipmentName}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Area:</span> {Math.round(application.totalSectionArea || application.sectionArea || 0).toLocaleString()} sq ft
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Products */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<h5 className="font-medium mb-2">Products to Apply:</h5>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{application.products?.map((product, index) => (
|
|
||||||
<div key={index} className="bg-white p-2 rounded border text-sm">
|
|
||||||
<div className="font-medium">{product.productName}</div>
|
|
||||||
<div className="text-gray-600">
|
|
||||||
Rate: {product.rateAmount} {product.rateUnit}
|
|
||||||
{product.productAmount && ` • Product: ${product.productAmount}`}
|
|
||||||
{product.waterAmount && ` • Water: ${product.waterAmount}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Speed Guidance for Liquid Applications */}
|
|
||||||
{targetSpeed && (
|
|
||||||
<div className={`p-4 rounded-lg mb-6 ${
|
|
||||||
speedStatus === 'normal' ? 'bg-green-50 border border-green-200' :
|
|
||||||
speedStatus === 'slow' ? 'bg-yellow-50 border border-yellow-200' :
|
|
||||||
'bg-red-50 border border-red-200'
|
|
||||||
}`}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium">Speed Guidance</h4>
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Target: {targetSpeed.toFixed(1)} mph • Current: {currentSpeed.toFixed(1)} mph
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={`text-lg font-bold ${
|
|
||||||
speedStatus === 'normal' ? 'text-green-600' :
|
|
||||||
speedStatus === 'slow' ? 'text-yellow-600' :
|
|
||||||
'text-red-600'
|
|
||||||
}`}>
|
|
||||||
{speedStatus === 'normal' ? '✓ Good Speed' :
|
|
||||||
speedStatus === 'slow' ? '↑ Go Faster' :
|
|
||||||
'↓ Slow Down'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Map with GPS Track */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<h4 className="font-medium mb-2">Application Area & GPS Track</h4>
|
|
||||||
<div className="h-96 border rounded-lg overflow-hidden">
|
|
||||||
{propertyDetails && (
|
|
||||||
<PropertyMap
|
|
||||||
property={propertyDetails}
|
|
||||||
selectedSections={application.sections?.map(s => s.id) || []}
|
|
||||||
mode="execution"
|
|
||||||
gpsTrack={gpsTrack}
|
|
||||||
currentLocation={currentLocation}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tracking Stats */}
|
|
||||||
{isTracking && (
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg mb-6">
|
|
||||||
<h4 className="font-medium mb-2">Tracking Statistics</h4>
|
|
||||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Duration:</span><br/>
|
|
||||||
{startTime ? Math.round((new Date() - startTime) / 60000) : 0} min
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Distance:</span><br/>
|
|
||||||
{(totalDistance * 3.28084).toFixed(0)} ft
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Avg Speed:</span><br/>
|
|
||||||
{averageSpeed.toFixed(1)} mph
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Track Points:</span><br/>
|
|
||||||
{gpsTrack.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{!isTracking ? (
|
|
||||||
<button
|
|
||||||
onClick={startTracking}
|
|
||||||
className="btn-primary flex-1"
|
|
||||||
>
|
|
||||||
Start Application
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={stopTracking}
|
|
||||||
className="btn-secondary flex-1"
|
|
||||||
>
|
|
||||||
Pause Tracking
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={completeApplication}
|
|
||||||
className="btn-primary flex-1"
|
|
||||||
>
|
|
||||||
Complete Application
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="btn-secondary"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Applications;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user