This commit is contained in:
Jake Kasper
2025-09-02 13:06:02 -05:00
parent 5a16f42e1c
commit f37b43bf4f
4 changed files with 243 additions and 2 deletions

View File

@@ -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 (
<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 mx-4 max-h-[90vh] overflow-y-auto">
<div className="py-8 text-center text-gray-600">Loading mowing session</div>
</div>
</div>
);
}
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-6xl mx-4 max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Mowing Session Details</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
<XMarkIcon className="h-6 w-6" />
</button>
</div>
{/* Overview */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
<MapPinIcon className="h-5 w-5 mr-2" />
Property & Areas
</h3>
<div className="space-y-1 text-sm">
<p><span className="font-medium">Property:</span> {sessionDetails.property_name || session.property_name}</p>
<p><span className="font-medium">Areas:</span> {sessionDetails.section_names || session.section_names || sections.map(s=>s.name).join(', ')}</p>
<p><span className="font-medium">Cut Height:</span> {Number(sessionDetails.cut_height_inches || session.cut_height_inches).toFixed(2)}"</p>
<p><span className="font-medium">Direction:</span> {sessionDetails.direction || session.direction}</p>
</div>
</div>
<div className="bg-gray-50 p-4 rounded-lg">
<h3 className="font-semibold text-gray-900 mb-3 flex items-center">
<WrenchScrewdriverIcon className="h-5 w-5 mr-2" />
Equipment & Stats
</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<div><span className="font-medium">Mower:</span> {sessionDetails.equipment_name || session.equipment_name}</div>
<div><span className="font-medium">Avg Speed:</span> {(sessionDetails.average_speed_mph || sessionDetails.averageSpeed || 0).toFixed?.(1) || Number(sessionDetails.averageSpeed || 0).toFixed(1)} mph</div>
<div><span className="font-medium">Duration:</span> {Math.round((sessionDetails.duration_seconds || sessionDetails.durationSeconds || 0)/60)} min</div>
<div><span className="font-medium">Distance:</span> {Math.round(toFeet(sessionDetails.total_distance_meters || (sessionDetails.gps_track?.totalDistance || 0)))} ft</div>
</div>
</div>
</div>
{/* Map */}
<div className="mb-6">
<h4 className="font-medium mb-2">Mowed Areas & GPS Track</h4>
<div className="h-96 border rounded-lg overflow-hidden">
<PropertyMap
sections={sections}
selectedSections={sections.map(s => s.id)}
mode="view"
gpsTrack={gpsTrack}
center={mapCenter}
zoom={mapCenter ? 16 : 15}
direction={sessionDetails.direction || session.direction}
editable={false}
/>
</div>
</div>
{/* Notes */}
{sessionDetails.notes && (
<div className="mb-6">
<h3 className="font-semibold text-gray-900 mb-2">Notes</h3>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="text-gray-700">{sessionDetails.notes}</p>
</div>
</div>
)}
<div className="flex justify-end">
<button onClick={onClose} className="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">Close</button>
</div>
</div>
</div>
);
};
export default MowingSessionViewModal;