import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { MapContainer, TileLayer, Marker, Popup, Polygon, Polyline, useMapEvents, useMap } from 'react-leaflet'; import { Icon } from 'leaflet'; import * as turf from '@turf/turf'; import { PlusIcon, TrashIcon, ArrowLeftIcon, MapPinIcon, SwatchIcon, PencilIcon } from '@heroicons/react/24/outline'; import { propertiesAPI, applicationsAPI, mowingAPI } from '../../services/api'; import ApplicationViewModal from '../../components/Applications/ApplicationViewModal'; import MowingSessionViewModal from '../../components/Mowing/MowingSessionViewModal'; import { EyeIcon, CalendarIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import toast from 'react-hot-toast'; import 'leaflet/dist/leaflet.css'; // Simple tag input and suggestion components for grass types const COOL_SEASON_GRASSES = [ 'Turf Type Tall Fescue', 'Kentucky Bluegrass', 'Perennial Ryegrass', 'Fine Fescue', 'Creeping Red Fescue', 'Chewings Fescue', 'Hard Fescue', 'Annual Ryegrass' ]; const TagInput = ({ value = [], onChange }) => { const [input, setInput] = useState(''); const add = (val) => { const v = val.trim(); if (!v) return; if (!value.includes(v)) onChange([...(value||[]), v]); setInput(''); }; return (
{(value||[]).map((t) => ( {t} ))}
setInput(e.target.value)} onKeyDown={(e)=> { if (e.key==='Enter'){ e.preventDefault(); add(input); } }} />
); }; const SuggestionChips = ({ onPick }) => (
{COOL_SEASON_GRASSES.map(g => ( ))}
); // Fix for default markers delete Icon.Default.prototype._getIconUrl; Icon.Default.mergeOptions({ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', }); // Colors for lawn sections const SECTION_COLORS = [ { name: 'Green', value: '#22c55e' }, { name: 'Blue', value: '#3b82f6' }, { name: 'Red', value: '#ef4444' }, { name: 'Yellow', value: '#eab308' }, { name: 'Purple', value: '#a855f7' }, { name: 'Orange', value: '#f97316' }, ]; // Calculate area in square feet const calculateAreaInSqFt = (coordinates) => { try { const polygon = turf.polygon([coordinates.map(coord => [coord[1], coord[0]])]); const areaInSqMeters = turf.area(polygon); return Math.round(areaInSqMeters * 10.7639); // Convert to sq ft } catch (error) { console.error('Area calculation error:', error); return 0; } }; // Calculate distance from a point to a line segment const pointToLineDistance = (point, lineStart, lineEnd) => { const [px, py] = point; const [x1, y1] = lineStart; const [x2, y2] = lineEnd; const A = px - x1; const B = py - y1; const C = x2 - x1; const D = y2 - y1; const dot = A * C + B * D; const lenSq = C * C + D * D; if (lenSq === 0) return Math.sqrt(A * A + B * B); let param = dot / lenSq; let xx, yy; if (param < 0) { xx = x1; yy = y1; } else if (param > 1) { xx = x2; yy = y2; } else { xx = x1 + param * C; yy = y1 + param * D; } const dx = px - xx; const dy = py - yy; return Math.sqrt(dx * dx + dy * dy); }; // Component for drawing polygons function PolygonDrawer({ isDrawing, onPolygonComplete, currentColor }) { const [currentPolygon, setCurrentPolygon] = useState([]); useMapEvents({ click(e) { if (!isDrawing) return; const newPoint = [e.latlng.lat, e.latlng.lng]; console.log('Adding point:', newPoint); setCurrentPolygon(prev => { const newPoly = [...prev, newPoint]; console.log('Current polygon has', newPoly.length, 'points'); return newPoly; }); }, dblclick(e) { console.log('Double click detected, drawing:', isDrawing, 'points:', currentPolygon.length); if (isDrawing && currentPolygon.length >= 3) { e.originalEvent.preventDefault(); e.originalEvent.stopPropagation(); console.log('Completing polygon with', currentPolygon.length, 'points'); onPolygonComplete(currentPolygon); setCurrentPolygon([]); } } }); return currentPolygon.length > 0 ? ( ) : null; } // Component for editable polygons function EditablePolygon({ section, onUpdate, onEdit, onDelete }) { // Import toast function from the module scope const [isEditing, setIsEditing] = useState(false); const [editedCoordinates, setEditedCoordinates] = useState(section.coordinates); const map = useMap(); const handleMarkerDrag = (index, newLatLng) => { const newCoords = [...editedCoordinates]; newCoords[index] = [newLatLng.lat, newLatLng.lng]; setEditedCoordinates(newCoords); // Recalculate area const area = calculateAreaInSqFt([...newCoords, newCoords[0]]); onUpdate(section.id, { ...section, coordinates: newCoords, area }); }; const addPointAtIndex = (index, e) => { e.originalEvent.stopPropagation(); const newCoords = [...editedCoordinates]; const newPoint = [e.latlng.lat, e.latlng.lng]; newCoords.splice(index + 1, 0, newPoint); setEditedCoordinates(newCoords); const area = calculateAreaInSqFt([...newCoords, newCoords[0]]); onUpdate(section.id, { ...section, coordinates: newCoords, area }); }; const removePoint = (index) => { if (editedCoordinates.length <= 3) { toast.error('Polygon must have at least 3 points'); return; } const newCoords = editedCoordinates.filter((_, i) => i !== index); setEditedCoordinates(newCoords); const area = calculateAreaInSqFt([...newCoords, newCoords[0]]); onUpdate(section.id, { ...section, coordinates: newCoords, area }); }; return ( <> { if (!isEditing) { setIsEditing(true); toast('Edit mode enabled: Drag points to move, right-click to remove points, click edges to add points'); } else { // Add point when clicking on polygon edge during edit mode const newPoint = [e.latlng.lat, e.latlng.lng]; // Find the closest edge and insert the point there let closestEdgeIndex = 0; let minDistance = Infinity; for (let i = 0; i < editedCoordinates.length; i++) { const start = editedCoordinates[i]; const end = editedCoordinates[(i + 1) % editedCoordinates.length]; // Calculate distance from clicked point to edge const distance = pointToLineDistance(newPoint, start, end); if (distance < minDistance) { minDistance = distance; closestEdgeIndex = i; } } // Insert new point after the closest edge start point const newCoords = [...editedCoordinates]; newCoords.splice(closestEdgeIndex + 1, 0, newPoint); setEditedCoordinates(newCoords); const area = calculateAreaInSqFt([...newCoords, newCoords[0]]); onUpdate(section.id, { ...section, coordinates: newCoords, area }); toast('Point added! Drag it to adjust position'); } } }} >
{section.name}
{section.area.toLocaleString()} sq ft
{/* Editable markers for each point */} {isEditing && editedCoordinates.map((coord, index) => ( { handleMarkerDrag(index, e.target.getLatLng()); }, contextmenu: (e) => { e.originalEvent.preventDefault(); if (editedCoordinates.length > 3) { removePoint(index); } } }} icon={new Icon({ iconUrl: 'data:image/svg+xml;base64,' + btoa(` `), iconSize: [12, 12], iconAnchor: [6, 6] })} >

Point {index + 1}

))} ); } const PropertyDetail = () => { const { id } = useParams(); const navigate = useNavigate(); const [property, setProperty] = useState(null); const [lawnSections, setLawnSections] = useState([]); const [loading, setLoading] = useState(true); const [isDrawing, setIsDrawing] = useState(false); // GPS capture modes const [isGPSPointsMode, setIsGPSPointsMode] = useState(false); // mark-at-location const [isGPSTraceMode, setIsGPSTraceMode] = useState(false); // continuous trace const [isTracing, setIsTracing] = useState(false); const [gpsWatchId, setGpsWatchId] = useState(null); const [gpsTracePoints, setGpsTracePoints] = useState([]); const [gpsSamples, setGpsSamples] = useState([]); // recent raw samples for smoothing const [gpsDistance, setGpsDistance] = useState(0); const [gpsAccuracy, setGpsAccuracy] = useState(null); const [isSnapPreview, setIsSnapPreview] = useState(false); const [showAddMenu, setShowAddMenu] = useState(false); // Modal picker to ensure options show reliably on mobile const [showCaptureModeModal, setShowCaptureModeModal] = useState(false); const [currentColor, setCurrentColor] = useState(SECTION_COLORS[0]); const [showNameModal, setShowNameModal] = useState(false); const [pendingSection, setPendingSection] = useState(null); const [sectionName, setSectionName] = useState(''); const [sectionGrassTypes, setSectionGrassTypes] = useState([]); const [editingSection, setEditingSection] = useState(null); const [showEditModal, setShowEditModal] = useState(false); const [editGrassTypes, setEditGrassTypes] = useState([]); // Recent history state for this property const [completedApplications, setCompletedApplications] = useState([]); const [applicationLogs, setApplicationLogs] = useState([]); const [mowingLogs, setMowingLogs] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [viewingApplication, setViewingApplication] = useState(null); const [viewingMowingSession, setViewingMowingSession] = useState(null); useEffect(() => { fetchPropertyDetails(); }, [id]); // eslint-disable-line react-hooks/exhaustive-deps // Load recent history when property is available useEffect(() => { if (!property?.id) return; const loadHistory = async () => { try { setHistoryLoading(true); const [completedRes, archivedRes, logsRes, mowRes] = await Promise.all([ applicationsAPI.getPlans({ status: 'completed', property_id: property.id }), applicationsAPI.getPlans({ status: 'archived', property_id: property.id }), applicationsAPI.getLogs({ property_id: property.id }), mowingAPI.getLogs({ property_id: property.id }) ]); const completedPlans = completedRes.data?.data?.plans || []; const archivedPlans = archivedRes.data?.data?.plans || []; setCompletedApplications([...(completedPlans||[]), ...(archivedPlans||[])]); setApplicationLogs(logsRes.data?.data?.logs || []); setMowingLogs(mowRes.data?.data?.logs || []); } catch (e) { console.warn('Failed to load property history', e?.response?.data || e.message); setCompletedApplications([]); setApplicationLogs([]); setMowingLogs([]); } finally { setHistoryLoading(false); } }; loadHistory(); }, [property?.id]); // Handle keyboard shortcuts for polygon drawing useEffect(() => { const handleKeyPress = (e) => { if (isDrawing && e.key === 'Enter') { // Find the PolygonDrawer component and complete the polygon console.log('Enter pressed during drawing mode'); } if (e.key === 'Escape' && isDrawing) { setIsDrawing(false); toast('Drawing cancelled'); } }; document.addEventListener('keydown', handleKeyPress); return () => document.removeEventListener('keydown', handleKeyPress); }, [isDrawing]); const fetchPropertyDetails = async () => { try { setLoading(true); const response = await propertiesAPI.getById(id); console.log('Property details:', response); const propertyData = response.data.data.property; setProperty(propertyData); // Convert backend sections to frontend format if (propertyData.sections && propertyData.sections.length > 0) { const convertedSections = propertyData.sections.map(section => { const polyRaw = typeof section.polygonData === 'string' ? JSON.parse(section.polygonData) : section.polygonData; const ring = Array.isArray(polyRaw?.coordinates?.[0]) ? polyRaw.coordinates[0] : []; const coords = ring.map(pt => [Number(pt[0]), Number(pt[1])]); const colorRaw = polyRaw?.color; const color = (colorRaw && typeof colorRaw === 'object' && 'value' in colorRaw) ? colorRaw : (typeof colorRaw === 'string' ? { name: 'Custom', value: colorRaw } : SECTION_COLORS[0]); return { id: section.id, name: section.name, coordinates: coords, color, area: section.area, grassType: section.grassType || '', grassTypes: section.grassTypes || null }; }); setLawnSections(convertedSections); } } catch (error) { console.error('Failed to fetch property:', error); toast.error('Failed to load property'); navigate('/properties'); } finally { setLoading(false); } }; const handlePolygonComplete = (coordinates) => { console.log('handlePolygonComplete called with', coordinates.length, 'coordinates'); if (coordinates.length < 3) { toast.error('Polygon needs at least 3 points'); return; } const area = calculateAreaInSqFt([...coordinates, coordinates[0]]); console.log('Calculated area:', area); setPendingSection({ coordinates, color: currentColor, area }); setShowNameModal(true); setIsDrawing(false); }; // Haversine distance in meters const haversine = (lat1, lon1, lat2, lon2) => { const toRad = (d) => (d * Math.PI) / 180; const R = 6371000; const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat/2)**2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon/2)**2; return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }; // GPS filtering and snapping (defaults) const ACCURACY_MAX_METERS = 12; // Ignore points with accuracy worse than this const MIN_MOVE_METERS = 2.5; // Ignore points if user hasn't moved this far from last point const TURN_MIN_DEG = 12; // Minimum heading change to record when small movement const SNAP_METERS = 5; // Snap to starting point when within this distance // Compute bearing between two lat/lng points (degrees) const bearingDeg = (a, b) => { const toRad = (d) => (d * Math.PI) / 180; const toDeg = (r) => (r * 180) / Math.PI; const lat1 = toRad(a[0]); const lat2 = toRad(b[0]); const dLng = toRad(b[1] - a[1]); const y = Math.sin(dLng) * Math.cos(lat2); const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLng); const brg = (toDeg(Math.atan2(y, x)) + 360) % 360; return brg; }; // Maintain a short buffer of samples; return averaged lat/lng of best by accuracy const addSampleAndSmooth = (lat, lng, accuracy) => { const now = Date.now(); setGpsSamples((prev) => [...prev, { lat, lng, accuracy: accuracy ?? 999, t: now }].slice(-12)); const pool = [...gpsSamples, { lat, lng, accuracy: accuracy ?? 999, t: now }].slice(-12); const top = pool .filter((s) => s.accuracy != null) .sort((a, b) => a.accuracy - b.accuracy) .slice(0, Math.min(5, pool.length)); if (top.length === 0) return [lat, lng]; const avgLat = top.reduce((s, p) => s + p.lat, 0) / top.length; const avgLng = top.reduce((s, p) => s + p.lng, 0) / top.length; return [avgLat, avgLng]; }; // Get the most accurate recent sample (prefer samples within 3s) const bestRecentSample = () => { const now = Date.now(); const recent = gpsSamples.filter(s => (now - (s.t || 0)) <= 3000); const pool = (recent.length ? recent : gpsSamples).slice(-12); if (pool.length === 0) return null; const best = pool.reduce((b, s) => (s.accuracy < (b?.accuracy ?? Infinity) ? s : b), null); return best ? [best.lat, best.lng, best.accuracy] : null; }; const acceptAndNormalizePoint = (lat, lng, accuracy, currentPoints) => { if (accuracy != null && accuracy > ACCURACY_MAX_METERS) { toast("GPS accuracy too low (" + Math.round(accuracy) + "m). Waiting for better fix…"); return null; } // Movement threshold if (currentPoints.length > 0) { const [llat, llng] = currentPoints[currentPoints.length - 1]; const moved = haversine(llat, llng, lat, lng); if (moved < MIN_MOVE_METERS) { // Allow small move only if heading changed sufficiently vs previous segment if (currentPoints.length >= 2) { const prevHead = bearingDeg(currentPoints[currentPoints.length - 2], [llat, llng]); const candHead = bearingDeg([llat, llng], [lat, lng]); let diff = Math.abs(prevHead - candHead); if (diff > 180) diff = 360 - diff; if (diff < TURN_MIN_DEG) return null; } else { return null; } } } // Snap to starting point if near if (currentPoints.length >= 2) { const [slat, slng] = currentPoints[0]; const dStart = haversine(slat, slng, lat, lng); if (dStart <= SNAP_METERS) { return [slat, slng]; } } return [lat, lng]; }; // GPS point collection (mark-at-location) const markCurrentPoint = () => { if (!navigator.geolocation) { toast.error('GPS not available'); return; } // Prefer most accurate recent fix from warm watch; fall back to immediate read const candidate = bestRecentSample(); const applyFix = (lat, lng, acc) => { setGpsAccuracy(acc || null); setGpsTracePoints(prev => { const normalized = acceptAndNormalizePoint(lat, lng, acc, prev); if (prev.length >= 2) { const [slat, slng] = prev[0]; const dStart = haversine(slat, slng, lat, lng); setIsSnapPreview(dStart <= SNAP_METERS); } else { setIsSnapPreview(false); } if (!normalized) return prev; // skip const next = [...prev, normalized]; if (next.length > 1) { const [pl, pg] = next[next.length - 2]; setGpsDistance(d => d + haversine(pl, pg, normalized[0], normalized[1])); } return next; }); }; if (candidate) { const [lat, lng, acc] = candidate; applyFix(lat, lng, acc); } else { navigator.geolocation.getCurrentPosition((pos) => { const { latitude, longitude, accuracy } = pos.coords; addSampleAndSmooth(latitude, longitude, accuracy); applyFix(latitude, longitude, accuracy); }, (err)=>{ console.warn('GPS error', err?.message); toast.error('GPS error: ' + (err?.message || 'unknown')); }, { enableHighAccuracy: true, maximumAge: 500, timeout: 8000 }); } }; const undoLastPoint = () => { setGpsTracePoints(prev => { if (prev.length <= 0) return prev; const next = prev.slice(0, -1); // Recompute distance let dist = 0; for (let i=1;i { setGpsTracePoints([]); setGpsDistance(0); setGpsAccuracy(null); // Stop passive watch if running (used for Points mode warmup) if (gpsWatchId) { navigator.geolocation.clearWatch(gpsWatchId); setGpsWatchId(null); } }; const completeTracing = () => { if (gpsTracePoints.length < 3) { toast.error('Need at least 3 points to create an area'); return; } // Close polygon by ensuring first == last handled in save handlePolygonComplete(gpsTracePoints); setIsGPSTraceMode(false); setIsGPSPointsMode(false); clearGpsPoints(); }; const saveLawnSection = async () => { if (!sectionName.trim()) { toast.error('Please enter a section name'); return; } try { // Ensure closed ring and recompute area let coords = pendingSection.coordinates || []; if (coords.length >= 3) { const [f0, f1] = coords[0] || []; const [l0, l1] = coords[coords.length - 1] || []; if (f0 !== l0 || f1 !== l1) coords = [...coords, coords[0]]; } if (coords.length < 4) { toast.error('Polygon invalid: need at least 3 unique points'); return; } const recomputedArea = calculateAreaInSqFt(coords); if (!recomputedArea || recomputedArea <= 0) { toast.error('Polygon area is zero — adjust points'); return; } const sectionData = { name: sectionName, area: recomputedArea, polygonData: { coordinates: [coords], color: pendingSection.color }, grassType: sectionGrassTypes.join(', '), grassTypes: sectionGrassTypes, soilType: null, captureMethod: isGPSPointsMode ? 'gps_points' : (isGPSTraceMode ? 'gps_trace' : 'tap'), captureMeta: { accuracyLast: gpsAccuracy, totalDistanceMeters: gpsDistance, pointsCount: pendingSection.coordinates?.length || 0 } }; const response = await propertiesAPI.createSection(id, sectionData); const savedSection = response.data.data.section; // Convert backend format to frontend format; parse polygonData if string const poly = typeof savedSection.polygonData === 'string' ? JSON.parse(savedSection.polygonData) : savedSection.polygonData; const ring = Array.isArray(poly?.coordinates?.[0]) ? poly.coordinates[0] : []; const coordsNorm = ring.map(pt => [Number(pt[0]), Number(pt[1])]); const colorRaw = poly?.color; const colorNorm = (colorRaw && typeof colorRaw === 'object' && 'value' in colorRaw) ? colorRaw : (typeof colorRaw === 'string' ? { name: 'Custom', value: colorRaw } : currentColor); const newSection = { id: savedSection.id, name: savedSection.name, coordinates: coordsNorm, color: colorNorm, area: savedSection.area, grassType: savedSection.grassType || '', grassTypes: savedSection.grassTypes || [] }; setLawnSections(prev => [...prev, newSection]); toast.success(`${sectionName} section created and saved!`); // Reset and cycle color setSectionName(''); setSectionGrassTypes([]); setPendingSection(null); setShowNameModal(false); const nextIndex = (SECTION_COLORS.findIndex(c => c.value === currentColor.value) + 1) % SECTION_COLORS.length; setCurrentColor(SECTION_COLORS[nextIndex]); } catch (error) { console.error('Failed to save section:', error); toast.error(error?.response?.data?.message || error?.message || 'Failed to save section. Please try again.'); } }; const deleteLawnSection = async (sectionId) => { if (window.confirm('Delete this lawn section?')) { try { await propertiesAPI.deleteSection(id, sectionId); setLawnSections(prev => prev.filter(s => s.id !== sectionId)); toast.success('Section deleted'); } catch (error) { console.error('Failed to delete section:', error); toast.error('Failed to delete section. Please try again.'); } } }; const startEditSection = (section) => { setEditingSection(section); setSectionName(section.name); setCurrentColor(section.color); setShowEditModal(true); const existing = (section.grassType || '').split(',').map(s=>s.trim()).filter(Boolean); setEditGrassTypes(existing); }; const saveEditedSection = async () => { if (!sectionName.trim()) { toast.error('Please enter a section name'); return; } try { const sectionData = { name: sectionName, area: editingSection.area, polygonData: { coordinates: [editingSection.coordinates], color: currentColor }, grassType: editGrassTypes.join(', '), grassTypes: editGrassTypes, soilType: null }; await propertiesAPI.updateSection(id, editingSection.id, sectionData); const updatedSection = { ...editingSection, name: sectionName, color: currentColor, grassType: editGrassTypes.join(', '), grassTypes: [...editGrassTypes] }; setLawnSections(prev => prev.map(s => s.id === editingSection.id ? updatedSection : s)); toast.success('Section updated and saved!'); // Reset setSectionName(''); setEditingSection(null); setShowEditModal(false); setEditGrassTypes([]); } catch (error) { console.error('Failed to update section:', error); toast.error('Failed to update section. Please try again.'); } }; const updateSection = async (sectionId, updatedSection) => { try { const sectionData = { name: updatedSection.name, area: updatedSection.area, polygonData: { coordinates: [updatedSection.coordinates], color: updatedSection.color }, grassType: (updatedSection.grassType || ''), grassTypes: (updatedSection.grassTypes || null), soilType: null }; await propertiesAPI.updateSection(id, sectionId, sectionData); setLawnSections(prev => prev.map(s => s.id === sectionId ? updatedSection : s)); } catch (error) { console.error('Failed to update section coordinates:', error); toast.error('Failed to save polygon changes. Please try again.'); } }; const getTotalArea = () => { return lawnSections.reduce((total, section) => total + section.area, 0); }; // History helpers const getApplicationType = (app) => { if (!app.productDetails || app.productDetails.length === 0) return null; return app.productDetails[0].type; }; const calculateCoverage = (application, log) => { if (!log?.gpsTrack?.points && !log?.gps_track?.points) return 0; const storedMeters = typeof (log.gpsTrack?.totalDistance || log.gps_track?.totalDistance) === 'number' ? (log.gpsTrack?.totalDistance || log.gps_track?.totalDistance) : 0; const totalDistanceFeet = storedMeters * 3.28084; const plannedArea = application.totalSectionArea || application.total_section_area || 0; if (totalDistanceFeet === 0 || plannedArea === 0) return 0; let equipmentWidth = 4; const equipmentName = (application.equipmentName || '').toLowerCase(); if (equipmentName.includes('spreader')) equipmentWidth = 12; else if (equipmentName.includes('sprayer')) equipmentWidth = 20; else if (equipmentName.includes('mower')) equipmentWidth = 6; const theoreticalCoverageArea = totalDistanceFeet * equipmentWidth; return Math.min(Math.round((theoreticalCoverageArea / plannedArea) * 100), 100); }; const calculateMowingCoverage = (log) => { const meters = (log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0) || 0; const totalDistanceFeet = meters * 3.28084; const plannedArea = Number(log.total_area || 0); const widthInches = parseFloat(log.cutting_width_inches || 0); const widthFeet = isNaN(widthInches) ? 0 : (widthInches / 12); if (totalDistanceFeet === 0 || plannedArea === 0 || widthFeet === 0) return 0; const theoreticalCoverageArea = totalDistanceFeet * widthFeet; return Math.min(100, Math.round((theoreticalCoverageArea / plannedArea) * 100)); }; const unifiedHistory = (() => { const apps = (completedApplications||[]).map((application) => { const log = applicationLogs.find((l) => l.planId === application.id); const dateStr = log?.applicationDate || application.plannedDate || application.updatedAt || application.createdAt; const date = dateStr ? new Date(dateStr) : new Date(0); return { kind: 'application', date, application, log }; }); const mows = (mowingLogs||[]).map((log) => { const dateStr = log.session_date || log.created_at; const date = dateStr ? new Date(dateStr) : new Date(0); return { kind: 'mowing', date, log }; }); const items = [...apps, ...mows]; items.sort((a,b)=> b.date - a.date); return items.slice(0, 10); })(); if (loading) { return (
); } if (!property) { return (

Property Not Found

); } // Validate coordinates and provide fallbacks const hasValidCoordinates = property && property.latitude !== undefined && property.longitude !== undefined && property.latitude !== null && property.longitude !== null && !isNaN(property.latitude) && !isNaN(property.longitude); const mapCenter = hasValidCoordinates ? [property.latitude, property.longitude] : [40.7128, -74.0060]; // Default to NYC return (
{/* Header */}

{property.name}

{property.address || 'No address specified'}

{!hasValidCoordinates && (

⚠️ Property coordinates not set - using default location

)}
{/* Dropdown removed in favor of modal for reliability */}
{/* Map */}
{hasValidCoordinates && ( {property.name} )} {lawnSections.map((section) => ( ))} {isDrawing && ( )} {/* GPS trace overlays */} {(isGPSPointsMode || isGPSTraceMode) && gpsTracePoints.length > 0 && ( <> {gpsTracePoints.map((p, i) => ( ${i===0 && isSnapPreview ? '' : ''} `), iconSize: [14,14], iconAnchor: [7,7] })} /> ))} )}
{isDrawing && (

Drawing Mode Active: Click to add points. Double-click to complete polygon.

Need at least 3 points to create a section. Press ESC to cancel.

💡 After creating: Click any polygon to edit its points by dragging

)} {isGPSPointsMode && (
GPS Points Mode Points: {gpsTracePoints.length} Distance: {(gpsDistance * 3.28084).toFixed(0)} ft {gpsAccuracy != null && Accuracy: ±{Math.round(gpsAccuracy)} m}
{isSnapPreview && Snap to start available}

Walk to each corner, tap Mark Point, then Complete. You can refine points afterward.

)} {isGPSTraceMode && (
Trace Mode Points: {gpsTracePoints.length} Distance: {(gpsDistance * 3.28084).toFixed(0)} ft {gpsAccuracy != null && Accuracy: ±{Math.round(gpsAccuracy)} m}
{!isTracing ? ( ) : ( )} {isSnapPreview && Snap to start available}

Walk the boundary to trace it. Pause as needed, then Complete to create the area.

)}
{/* Sidebar */}
{/* Color Selector */} {isDrawing && (

Section Color

{SECTION_COLORS.map((color) => (
)} {/* Property Summary */}

Property Summary

Total Sections: {lawnSections.length}
Total Area: {getTotalArea().toLocaleString()} sq ft
{/* Lawn Sections */}

Lawn Sections

{lawnSections.length === 0 ? (

No sections yet.

) : (
{lawnSections.map((section) => (

{section.name}

{section.area.toLocaleString()} sq ft

{(section.grassTypes && section.grassTypes.length > 0) || section.grassType ? (
{((section.grassTypes && section.grassTypes.length > 0) ? section.grassTypes : (section.grassType||'').split(',').map(s=>s.trim()).filter(Boolean)).map((g, idx)=>( {g} ))}
) : null}
))}
)}
{/* Capture Mode Modal */} {showCaptureModeModal && (

Create Lawn Section

)} {/* Recent History */}

Recent History

{historyLoading ? (
Loading…
) : unifiedHistory.length === 0 ? (
No history yet for this property.
) : (
{unifiedHistory.map((item) => { if (item.kind === 'application') { const application = item.application; const log = item.log; return (

{application.propertyName} - {application.sectionNames}

Application
{application.status==='archived'?'Archived':'Completed'}
{new Date(item.date).toLocaleString()}
{application.propertyName}
{application.equipmentName}
{log?.gpsTrack && (
Duration
{Math.round((log.gpsTrack?.duration||0)/60)} min
GPS Points
{log.gpsTrack?.points?.length || 0}
Distance
{Math.round(log.gpsTrack?.totalDistance||0)} ft
Coverage
{calculateCoverage(application, log)}%
)}
); } const log = item.log; const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || log.gps_track?.duration || 0)/60); const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || log.gps_track?.totalDistance || 0)*3.28084)||0); return (

{log.property_name} - {log.section_names}

Mowing
Completed
{new Date(item.date).toLocaleString()}
{log.property_name}
{log.equipment_name || 'Mower'}
Duration
{durationMin} min
GPS Points
{log.gpsTrack?.points?.length || log.gps_track?.points?.length || 0}
Distance
{distFeet} ft
Coverage
{calculateMowingCoverage(log)}%
); })}
)}
{/* Name Modal */} {showNameModal && (

Name Your Lawn Section

setSectionName(e.target.value)} placeholder="e.g., Front Yard, Back Lawn" autoFocus />
{pendingSection?.area.toLocaleString()} sq ft
{/* Nudge controls */}
setSectionGrassTypes(prev=> prev.includes(v)? prev : [...prev, v])} />
)} {/* Edit Modal */} {showEditModal && (

Edit Lawn Section

setSectionName(e.target.value)} placeholder="e.g., Front Yard, Back Lawn" autoFocus />
{SECTION_COLORS.map((color) => (
setEditGrassTypes(prev=> prev.includes(v)? prev : [...prev, v])} />
{editingSection?.area.toLocaleString()} sq ft
)} {/* View Modals for History */} {viewingApplication && ( setViewingApplication(null)} /> )} {viewingMowingSession && ( setViewingMowingSession(null)} /> )}
); }; export default PropertyDetail;