From f37b43bf4fd98e65830dd312923ea0548d88a2ae Mon Sep 17 00:00:00 2001 From: Jake Kasper Date: Tue, 2 Sep 2025 13:06:02 -0500 Subject: [PATCH] zsdfv --- .../Mowing/MowingSessionViewModal.js | 148 ++++++++++++++++++ frontend/src/pages/History/History.js | 54 ++++++- frontend/src/pages/Mowing/Mowing.js | 42 ++++- frontend/src/services/api.js | 1 + 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Mowing/MowingSessionViewModal.js diff --git a/frontend/src/components/Mowing/MowingSessionViewModal.js b/frontend/src/components/Mowing/MowingSessionViewModal.js new file mode 100644 index 0000000..dbaf0bd --- /dev/null +++ b/frontend/src/components/Mowing/MowingSessionViewModal.js @@ -0,0 +1,148 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { mowingAPI, propertiesAPI } from '../../services/api'; +import PropertyMap from '../Maps/PropertyMap'; +import { XMarkIcon, ClockIcon, MapPinIcon, WrenchScrewdriverIcon } from '@heroicons/react/24/outline'; + +const MowingSessionViewModal = ({ session, onClose }) => { + const [sections, setSections] = useState([]); + const [sessionDetails, setSessionDetails] = useState(session); + const [loading, setLoading] = useState(true); + + const toFeet = (meters) => (meters || 0) * 3.28084; + + useEffect(() => { + const load = async () => { + if (!session?.id) return; + try { + setLoading(true); + // Pull full session with related sections + const r = await mowingAPI.getSession(session.id); + const s = r.data?.data?.session || session; + const secs = r.data?.data?.sections || []; + setSessionDetails(s); + if (secs.length) { + setSections(secs); + } else if (s.property_id) { + const pr = await propertiesAPI.getById(s.property_id); + setSections(pr.data?.data?.property?.sections || []); + } + } catch (e) { + // Fallback: try property + try { + if (session.property_id) { + const pr = await propertiesAPI.getById(session.property_id); + setSections(pr.data?.data?.property?.sections || []); + } + } catch {} + } finally { + setLoading(false); + } + }; + load(); + }, [session?.id, session?.property_id]); + + // Normalize gps track + const gpsTrack = useMemo(() => { + const g = sessionDetails?.gps_track || sessionDetails?.gpsTrack || {}; + return g.points || []; + }, [sessionDetails]); + + const mapCenter = useMemo(() => { + if (!sections.length) return null; + let sumLat = 0, sumLng = 0, count = 0; + sections.forEach(sec => { + let poly = sec.polygonData || sec.polygon_data; + if (typeof poly === 'string') { try { poly = JSON.parse(poly); } catch { poly = null; } } + if (poly?.coordinates?.[0]) { + poly.coordinates[0].forEach(([lat, lng]) => { sumLat += lat; sumLng += lng; count++; }); + } + }); + return count ? [sumLat / count, sumLng / count] : null; + }, [sections]); + + if (loading) { + return ( +
+
+
Loading mowing session…
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+

Mowing Session Details

+ +
+ + {/* Overview */} +
+
+

+ + Property & Areas +

+
+

Property: {sessionDetails.property_name || session.property_name}

+

Areas: {sessionDetails.section_names || session.section_names || sections.map(s=>s.name).join(', ')}

+

Cut Height: {Number(sessionDetails.cut_height_inches || session.cut_height_inches).toFixed(2)}"

+

Direction: {sessionDetails.direction || session.direction}

+
+
+
+

+ + Equipment & Stats +

+
+
Mower: {sessionDetails.equipment_name || session.equipment_name}
+
Avg Speed: {(sessionDetails.average_speed_mph || sessionDetails.averageSpeed || 0).toFixed?.(1) || Number(sessionDetails.averageSpeed || 0).toFixed(1)} mph
+
Duration: {Math.round((sessionDetails.duration_seconds || sessionDetails.durationSeconds || 0)/60)} min
+
Distance: {Math.round(toFeet(sessionDetails.total_distance_meters || (sessionDetails.gps_track?.totalDistance || 0)))} ft
+
+
+
+ + {/* Map */} +
+

Mowed Areas & GPS Track

+
+ s.id)} + mode="view" + gpsTrack={gpsTrack} + center={mapCenter} + zoom={mapCenter ? 16 : 15} + direction={sessionDetails.direction || session.direction} + editable={false} + /> +
+
+ + {/* Notes */} + {sessionDetails.notes && ( +
+

Notes

+
+

{sessionDetails.notes}

+
+
+ )} + +
+ +
+
+
+ ); +}; + +export default MowingSessionViewModal; + diff --git a/frontend/src/pages/History/History.js b/frontend/src/pages/History/History.js index 68df813..f1630af 100644 --- a/frontend/src/pages/History/History.js +++ b/frontend/src/pages/History/History.js @@ -8,18 +8,21 @@ import { CalendarIcon, ChartBarIcon } from '@heroicons/react/24/outline'; -import { applicationsAPI } from '../../services/api'; +import { applicationsAPI, mowingAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import ApplicationViewModal from '../../components/Applications/ApplicationViewModal'; +import MowingSessionViewModal from '../../components/Mowing/MowingSessionViewModal'; import toast from 'react-hot-toast'; const History = () => { // State for applications and UI const [completedApplications, setCompletedApplications] = useState([]); const [applicationLogs, setApplicationLogs] = useState([]); + const [mowingLogs, setMowingLogs] = useState([]); const [loading, setLoading] = useState(true); const [showViewModal, setShowViewModal] = useState(false); const [viewingApplication, setViewingApplication] = useState(null); + const [viewingMowingSession, setViewingMowingSession] = useState(null); const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null); const [dateFilter, setDateFilter] = useState('all'); // all, today, week, month, custom const [dateRangeStart, setDateRangeStart] = useState(''); @@ -113,6 +116,15 @@ const History = () => { setCompletedApplications(allHistoryApplications); setApplicationLogs(logs); + + // Fetch mowing sessions/logs + try { + const mowingRes = await mowingAPI.getLogs(); + setMowingLogs(mowingRes.data?.data?.logs || []); + } catch (e) { + console.warn('Failed to load mowing logs', e?.response?.data || e.message); + setMowingLogs([]); + } } catch (error) { console.error('Failed to fetch history data:', error); @@ -262,6 +274,7 @@ const History = () => { const totalApplications = completedApplications.length; const totalAreaTreated = completedApplications.reduce((sum, app) => sum + (app.totalSectionArea || 0), 0); const totalDuration = applicationLogs.reduce((sum, log) => sum + (log.gpsTrack?.duration || 0), 0); + const totalMowingSessions = mowingLogs.length; if (loading) { return ( @@ -665,6 +678,37 @@ const History = () => { )} + {/* Mowing History */} + {mowingLogs.length > 0 && ( +
+
+

Mowing History

+

{totalMowingSessions} session{totalMowingSessions!==1?'s':''}

+
+
+ {mowingLogs.map((log) => { + const durationMin = Math.round((log.duration_seconds || log.durationSeconds || log.gpsTrack?.duration || 0) / 60); + const avg = (log.average_speed_mph || log.averageSpeed || 0).toFixed?.(1) || Number(log.averageSpeed || 0).toFixed(1); + const distFeet = Math.round(((log.total_distance_meters || log.gpsTrack?.totalDistance || 0) * 3.28084) || 0); + return ( +
+
+
{log.property_name || 'Property'}
+
{log.equipment_name || 'Mower'} • {avg} mph • {distFeet} ft • {durationMin} min
+
+ +
+ ); + })} +
+
+ )} + {/* Application View Modal */} {showViewModal && viewingApplication && ( { }} /> )} + + {/* Mowing Session View Modal */} + {viewingMowingSession && ( + setViewingMowingSession(null)} + /> + )} ); }; diff --git a/frontend/src/pages/Mowing/Mowing.js b/frontend/src/pages/Mowing/Mowing.js index 47993af..c2501d5 100644 --- a/frontend/src/pages/Mowing/Mowing.js +++ b/frontend/src/pages/Mowing/Mowing.js @@ -3,6 +3,7 @@ import { mowingAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import MowingPlanModal from '../../components/Mowing/MowingPlanModal'; import MowingExecutionModal from '../../components/Mowing/MowingExecutionModal'; +import MowingSessionViewModal from '../../components/Mowing/MowingSessionViewModal'; import toast from 'react-hot-toast'; const Mowing = () => { @@ -10,12 +11,19 @@ const Mowing = () => { const [loading, setLoading] = useState(true); const [showPlanModal, setShowPlanModal] = useState(false); const [execPlan, setExecPlan] = useState(null); + const [sessions, setSessions] = useState([]); + const [viewSession, setViewSession] = useState(null); const fetchPlans = async () => { try { setLoading(true); const r = await mowingAPI.getPlans(); setPlans(r.data.data.plans || []); + // also load recent sessions + try { + const s = await mowingAPI.getLogs(); + setSessions(s.data?.data?.logs || []); + } catch {} } catch (e) { toast.error('Failed to load mowing plans'); } finally { @@ -70,9 +78,41 @@ const Mowing = () => { {execPlan && ( setExecPlan(null)} onComplete={fetchPlans} /> )} + + {/* Recent Sessions */} +
+

Recent Sessions

+
+
+
Property
+
Mower
+
Cut Height
+
Avg Speed
+
Distance
+
Actions
+
+ {sessions.length === 0 ? ( +
No sessions yet.
+ ) : sessions.map((s) => ( +
+
{s.property_name}
+
{s.equipment_name || '—'}
+
{Number(s.cut_height_inches || 0).toFixed(2)}"
+
{(s.average_speed_mph || s.averageSpeed || 0).toFixed?.(1) || Number(s.averageSpeed || 0).toFixed(1)} mph
+
{Math.round(((s.total_distance_meters || s.gpsTrack?.totalDistance || 0) * 3.28084) || 0)} ft
+
+ +
+
+ ))} +
+
+ + {viewSession && ( + setViewSession(null)} /> + )} ); }; export default Mowing; - diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index a0bb04d..5fda2b7 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -245,6 +245,7 @@ export const mowingAPI = { // Logs/Sessions createLog: (data) => apiClient.post('/mowing/sessions', data), getLogs: () => apiClient.get('/mowing/logs'), + getSession: (id) => apiClient.get(`/mowing/sessions/${id}`), }; // Utility functions