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