import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { MapContainer, TileLayer, Marker, Popup, Polygon, 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); 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 => ({ id: section.id, name: section.name, coordinates: section.polygonData?.coordinates?.[0] || [], color: section.polygonData?.color || SECTION_COLORS[0], 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); }; const saveLawnSection = async () => { if (!sectionName.trim()) { toast.error('Please enter a section name'); return; } try { const sectionData = { name: sectionName, area: pendingSection.area, polygonData: { coordinates: [pendingSection.coordinates], color: pendingSection.color }, grassType: sectionGrassTypes.join(', '), grassTypes: sectionGrassTypes, soilType: null }; const response = await propertiesAPI.createSection(id, sectionData); const savedSection = response.data.data.section; // Convert backend format to frontend format const newSection = { id: savedSection.id, name: savedSection.name, coordinates: savedSection.polygonData.coordinates[0], color: savedSection.polygonData.color, area: savedSection.area }; 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('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 }; 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

)}
{/* Map */}
{hasValidCoordinates && ( {property.name} )} {lawnSections.map((section) => ( ))} {isDrawing && ( )}
{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

)}
{/* 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}
))}
)}
{/* 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
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;