Files
turftracker/frontend/src/pages/History/History.js
2025-09-03 10:04:34 -04:00

806 lines
34 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
ClockIcon,
MapPinIcon,
WrenchScrewdriverIcon,
BeakerIcon,
EyeIcon,
CalendarIcon,
ChartBarIcon
} from '@heroicons/react/24/outline';
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('');
const [dateRangeEnd, setDateRangeEnd] = useState('');
const [sortBy, setSortBy] = useState('date'); // date, area, duration
const [statusFilter, setStatusFilter] = useState('all'); // all, completed, archived
const [propertyFilter, setPropertyFilter] = useState('all');
const [selectedProducts, setSelectedProducts] = useState([]);
const [applicationTypeFilter, setApplicationTypeFilter] = useState('all'); // all, granular, liquid, mowing
const [showFilters, setShowFilters] = useState(false);
const [showProductDropdown, setShowProductDropdown] = useState(false);
// Haversine in meters
const haversineMeters = (lat1, lng1, lat2, lng2) => {
const R = 6371e3;
const toRad = (d) => (d * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLng = toRad(lng2 - lng1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
const computeDistanceFeetFromPoints = (points = []) => {
if (!Array.isArray(points) || points.length < 2) return 0;
let meters = 0;
for (let i = 1; i < points.length; i++) {
const p1 = points[i - 1];
const p2 = points[i];
meters += haversineMeters(p1.lat, p1.lng, p2.lat, p2.lng);
}
return meters * 3.28084;
};
// Calculate coverage percentage based on GPS tracking and equipment specifications
const calculateCoverage = (application, log) => {
if (!log?.gpsTrack?.points || log.gpsTrack.points.length < 2) return 0;
const storedMeters = typeof log.gpsTrack.totalDistance === 'number' ? log.gpsTrack.totalDistance : 0;
const totalDistanceFeet = storedMeters > 0 ? storedMeters * 3.28084 : computeDistanceFeetFromPoints(log.gpsTrack.points);
const plannedArea = application.totalSectionArea || 0;
if (totalDistanceFeet === 0 || plannedArea === 0) return 0;
// Estimate equipment width in feet
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;
const coveragePercentage = Math.min((theoreticalCoverageArea / plannedArea) * 100, 100);
return Math.round(coveragePercentage);
};
useEffect(() => {
fetchHistoryData();
}, []);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (showProductDropdown && !event.target.closest('.relative')) {
setShowProductDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showProductDropdown]);
const fetchHistoryData = async () => {
try {
setLoading(true);
// Fetch completed and archived applications
const [completedResponse, archivedResponse] = await Promise.all([
applicationsAPI.getPlans({ status: 'completed' }),
applicationsAPI.getPlans({ status: 'archived' })
]);
const completedPlans = completedResponse.data.data.plans || [];
const archivedPlans = archivedResponse.data.data.plans || [];
const allHistoryApplications = [...completedPlans, ...archivedPlans];
// Fetch application logs for additional details
const logsResponse = await applicationsAPI.getLogs();
const logs = logsResponse.data.data.logs || [];
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);
toast.error('Failed to load application history');
} finally {
setLoading(false);
}
};
const handleViewApplication = async (application) => {
try {
setViewingApplication(application);
setShowViewModal(true);
} catch (error) {
console.error('Failed to load application details:', error);
toast.error('Failed to load application details');
}
};
// Get application type from first product in productDetails
const getApplicationType = (app) => {
if (!app.productDetails || app.productDetails.length === 0) return null;
return app.productDetails[0].type; // granular or liquid
};
// Get unique values for filter options - dynamically filter based on other applied filters
const getFilteredApplicationsForOptions = () => {
return completedApplications.filter(app => {
// Apply all filters except products to get dynamic product list
if (dateFilter !== 'all') {
const appDate = new Date(app.plannedDate);
const now = new Date();
switch (dateFilter) {
case 'today':
if (appDate.toDateString() !== now.toDateString()) return false;
break;
case 'week':
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
if (appDate < weekAgo) return false;
break;
case 'month':
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
if (appDate < monthAgo) return false;
break;
case 'custom':
if (dateRangeStart) {
const startDate = new Date(dateRangeStart);
startDate.setHours(0, 0, 0, 0);
if (appDate < startDate) return false;
}
if (dateRangeEnd) {
const endDate = new Date(dateRangeEnd);
endDate.setHours(23, 59, 59, 999);
if (appDate > endDate) return false;
}
break;
}
}
if (statusFilter !== 'all' && app.status !== statusFilter) return false;
if (propertyFilter !== 'all' && app.propertyName !== propertyFilter) return false;
if (applicationTypeFilter !== 'all' && getApplicationType(app) !== applicationTypeFilter) return false;
return true;
});
};
const filteredForOptions = getFilteredApplicationsForOptions();
const uniqueProperties = [...new Set([
...completedApplications.map(app => app.propertyName),
...mowingLogs.map(log => log.property_name)
])].filter(Boolean);
const uniqueProducts = [...new Set(
filteredForOptions.flatMap(app =>
app.productDetails ? app.productDetails.map(p => p.name) : []
)
)].filter(Boolean).sort();
// Filter applications based on all filters
const filteredApplications = completedApplications.filter(app => {
// Date filter
if (dateFilter !== 'all') {
const appDate = new Date(app.plannedDate);
const now = new Date();
switch (dateFilter) {
case 'today':
if (appDate.toDateString() !== now.toDateString()) return false;
break;
case 'week':
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
if (appDate < weekAgo) return false;
break;
case 'month':
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
if (appDate < monthAgo) return false;
break;
case 'custom':
if (dateRangeStart) {
const startDate = new Date(dateRangeStart);
startDate.setHours(0, 0, 0, 0);
if (appDate < startDate) return false;
}
if (dateRangeEnd) {
const endDate = new Date(dateRangeEnd);
endDate.setHours(23, 59, 59, 999);
if (appDate > endDate) return false;
}
break;
}
}
// Status filter
if (statusFilter !== 'all' && app.status !== statusFilter) return false;
// Property filter
if (propertyFilter !== 'all' && app.propertyName !== propertyFilter) return false;
// Product filter - multi-select
if (selectedProducts.length > 0) {
if (!app.productDetails || !app.productDetails.some(p => selectedProducts.includes(p.name))) return false;
}
// Application type filter
if (applicationTypeFilter === 'mowing') return false; // hide app items when filtering for mowing only
if (applicationTypeFilter !== 'all' && applicationTypeFilter !== 'mowing' && getApplicationType(app) !== applicationTypeFilter) return false;
return true;
});
// Sort applications
const sortedApplications = [...filteredApplications].sort((a, b) => {
switch (sortBy) {
case 'date':
return new Date(b.plannedDate) - new Date(a.plannedDate);
case 'area':
return (b.totalSectionArea || 0) - (a.totalSectionArea || 0);
case 'duration':
const logA = applicationLogs.find(log => log.planId === a.id);
const logB = applicationLogs.find(log => log.planId === b.id);
return (logB?.gpsTrack?.duration || 0) - (logA?.gpsTrack?.duration || 0);
default:
return 0;
}
});
// Calculate summary statistics
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);
// Derive filtered + sorted mowing logs using shared filters (date/property)
const filteredMowingLogs = (mowingLogs || []).filter((log) => {
// Date filter (use session_date when available, fallback to created_at)
if (dateFilter !== 'all') {
const dStr = log.session_date || log.created_at;
const logDate = dStr ? new Date(dStr) : null;
if (!logDate) return false;
const now = new Date();
switch (dateFilter) {
case 'today':
if (logDate.toDateString() !== now.toDateString()) return false;
break;
case 'week': {
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
if (logDate < weekAgo) return false;
break;
}
case 'month': {
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
if (logDate < monthAgo) return false;
break;
}
case 'custom':
if (dateRangeStart) {
const startDate = new Date(dateRangeStart);
startDate.setHours(0, 0, 0, 0);
if (logDate < startDate) return false;
}
if (dateRangeEnd) {
const endDate = new Date(dateRangeEnd);
endDate.setHours(23, 59, 59, 999);
if (logDate > endDate) return false;
}
break;
default:
break;
}
}
// Property filter (property_name from API)
if (propertyFilter !== 'all' && (log.property_name || '') !== propertyFilter) return false;
return true;
});
const sortedMowingLogs = [...filteredMowingLogs].sort((a, b) => {
const aDate = a.session_date || a.created_at || 0;
const bDate = b.session_date || b.created_at || 0;
return new Date(bDate) - new Date(aDate);
});
const totalMowingSessions = sortedMowingLogs.length;
// Build unified history list (applications + mowing) sorted by date
const unifiedHistoryItems = (() => {
// Application items
const apps = [...filteredApplications].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 };
});
// Mowing items (already date-filtered and property-filtered above)
const mows = [...sortedMowingLogs].map((log) => {
const dateStr = log.session_date || log.created_at;
const date = dateStr ? new Date(dateStr) : new Date(0);
return { kind: 'mowing', date, log };
});
let items = [...apps, ...mows];
// Status filter: mowing sessions are treated as completed
if (statusFilter === 'archived') {
items = items.filter((it) => it.kind === 'application' && it.application.status === 'archived');
} else if (statusFilter === 'completed') {
items = items.filter((it) => (it.kind === 'application' && it.application.status === 'completed') || it.kind === 'mowing');
}
// Application Type filter across unified list
if (applicationTypeFilter === 'granular' || applicationTypeFilter === 'liquid') {
items = items.filter((it) => it.kind === 'application' && getApplicationType(it.application) === applicationTypeFilter);
} else if (applicationTypeFilter === 'mowing') {
items = items.filter((it) => it.kind === 'mowing');
}
// Sort newest first
items.sort((a, b) => b.date - a.date);
return items;
})();
if (loading) {
return (
<div className="p-6">
<LoadingSpinner />
</div>
);
}
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">History</h1>
<button
onClick={fetchHistoryData}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Refresh
</button>
</div>
{/* Summary Statistics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-blue-50 p-4 rounded-lg">
<div className="flex items-center">
<ChartBarIcon className="h-8 w-8 text-blue-600" />
<div className="ml-3">
<p className="text-sm text-blue-600 font-medium">Total Applications</p>
<p className="text-2xl font-bold text-blue-900">{totalApplications}</p>
</div>
</div>
</div>
<div className="bg-green-50 p-4 rounded-lg">
<div className="flex items-center">
<MapPinIcon className="h-8 w-8 text-green-600" />
<div className="ml-3">
<p className="text-sm text-green-600 font-medium">Total Area Treated</p>
<p className="text-2xl font-bold text-green-900">
{Math.round(totalAreaTreated / 1000)}k sq ft
</p>
</div>
</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg">
<div className="flex items-center">
<ClockIcon className="h-8 w-8 text-purple-600" />
<div className="ml-3">
<p className="text-sm text-purple-600 font-medium">Total Time</p>
<p className="text-2xl font-bold text-purple-900">
{Math.round(totalDuration / 3600)}h
</p>
</div>
</div>
</div>
</div>
{/* Filters and Sort */}
<div className="bg-white rounded-lg shadow mb-6">
<div className="p-4 border-b border-gray-200">
<button
onClick={() => setShowFilters(!showFilters)}
className="flex items-center justify-between w-full text-left"
>
<h3 className="text-lg font-medium text-gray-900">Filters & Sorting</h3>
<svg
className={`h-5 w-5 transform transition-transform ${showFilters ? 'rotate-180' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
{showFilters && (
<div className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Time Period
</label>
<select
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="custom">Custom Range</option>
</select>
{/* Date Range Inputs */}
{dateFilter === 'custom' && (
<div className="mt-2 grid grid-cols-2 gap-2">
<div>
<label className="block text-xs text-gray-600 mb-1">From</label>
<input
type="date"
value={dateRangeStart}
onChange={(e) => setDateRangeStart(e.target.value)}
className="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">To</label>
<input
type="date"
value={dateRangeEnd}
onChange={(e) => setDateRangeEnd(e.target.value)}
className="w-full border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Status
</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Status</option>
<option value="completed">Completed</option>
<option value="archived">Archived</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Property
</label>
<select
value={propertyFilter}
onChange={(e) => setPropertyFilter(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Properties</option>
{uniqueProperties.map(property => (
<option key={property} value={property}>{property}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Products
</label>
<div className="relative">
<button
onClick={() => setShowProductDropdown(!showProductDropdown)}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm text-left focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center justify-between"
>
<span>
{selectedProducts.length === 0
? 'All Products'
: `${selectedProducts.length} selected`
}
</span>
<svg className={`h-4 w-4 transform transition-transform ${showProductDropdown ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showProductDropdown && (
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto">
{uniqueProducts.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">No products available</div>
) : (
<>
<div className="px-3 py-2 border-b">
<button
onClick={() => {
if (selectedProducts.length === uniqueProducts.length) {
setSelectedProducts([]);
} else {
setSelectedProducts([...uniqueProducts]);
}
}}
className="text-xs text-blue-600 hover:text-blue-800"
>
{selectedProducts.length === uniqueProducts.length ? 'Deselect All' : 'Select All'}
</button>
</div>
{uniqueProducts.map(product => (
<label key={product} className="flex items-center px-3 py-2 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={selectedProducts.includes(product)}
onChange={(e) => {
if (e.target.checked) {
setSelectedProducts([...selectedProducts, product]);
} else {
setSelectedProducts(selectedProducts.filter(p => p !== product));
}
}}
className="mr-2"
/>
<span className="text-sm">{product}</span>
</label>
))}
</>
)}
</div>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Application Type
</label>
<select
value={applicationTypeFilter}
onChange={(e) => setApplicationTypeFilter(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Types</option>
<option value="granular">Granular</option>
<option value="liquid">Liquid</option>
<option value="mowing">Mowing</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sort By
</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="date">Date</option>
<option value="area">Area Size</option>
<option value="duration">Duration</option>
</select>
</div>
</div>
<div className="mt-4 pt-4 border-t border-gray-200">
<button
onClick={() => {
setDateFilter('all');
setDateRangeStart('');
setDateRangeEnd('');
setStatusFilter('all');
setPropertyFilter('all');
setSelectedProducts([]);
setApplicationTypeFilter('all');
setSortBy('date');
setShowProductDropdown(false);
}}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded hover:bg-gray-50"
>
Clear All Filters
</button>
</div>
</div>
)}
</div>
{/* Unified History List */}
{unifiedHistoryItems.length === 0 ? (
<div className="text-center py-12">
<CalendarIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No history items</h3>
<p className="text-gray-500">Try adjusting your filters or date range.</p>
</div>
) : (
<div className="grid gap-4">
{unifiedHistoryItems.map((item) => {
if (item.kind === 'application') {
const application = item.application;
const log = item.log;
return (
<div key={`app-${application.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">
{application.propertyName} - {application.sectionNames}
</h3>
<span className={`px-3 py-1 text-sm font-medium rounded-full ${
application.status === 'archived'
? 'bg-gray-100 text-gray-800'
: 'bg-green-100 text-green-800'
}`}>
{application.status === 'archived' ? 'Archived' : 'Completed'}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center text-sm text-gray-600">
<CalendarIcon className="h-4 w-4 mr-2" />
{new Date(item.date).toLocaleString()}
</div>
<div className="flex items-center text-sm text-gray-600">
<MapPinIcon className="h-4 w-4 mr-2" />
{application.propertyName}
</div>
<div className="flex items-center text-sm text-gray-600">
<WrenchScrewdriverIcon className="h-4 w-4 mr-2" />
{application.equipmentName}
</div>
</div>
{/* GPS Tracking Stats */}
{log?.gpsTrack && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-4">
<div className="bg-blue-50 p-2 rounded text-center">
<div className="text-xs text-blue-600 font-medium">Duration</div>
<div className="text-sm font-bold text-blue-900">
{Math.round((log.gpsTrack?.duration || 0) / 60)} min
</div>
</div>
<div className="bg-green-50 p-2 rounded text-center">
<div className="text-xs text-green-600 font-medium">GPS Points</div>
<div className="text-sm font-bold text-green-900">
{log.gpsTrack?.points?.length || 0}
</div>
</div>
<div className="bg-purple-50 p-2 rounded text-center">
<div className="text-xs text-purple-600 font-medium">Distance</div>
<div className="text-sm font-bold text-purple-900">
{Math.round(log.gpsTrack?.totalDistance || 0)} ft
</div>
</div>
<div className="bg-orange-50 p-2 rounded text-center">
<div className="text-xs text-orange-600 font-medium">Coverage</div>
<div className="text-sm font-bold text-orange-900">
{calculateCoverage(application, log)}%
</div>
</div>
</div>
)}
{/* Products */}
{application.productDetails && application.productDetails.length > 0 && (
<div className="mb-4">
<div className="flex items-center text-sm text-gray-600 mb-2">
<BeakerIcon className="h-4 w-4 mr-2" />
Products Applied:
</div>
<div className="flex flex-wrap gap-2">
{application.productDetails.map((product, index) => (
<span
key={index}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs"
>
{product.name} ({product.rateAmount} {product.rateUnit})
</span>
))}
</div>
</div>
)}
{application.notes && (
<p className="text-sm text-gray-600 mb-4">
<strong>Notes:</strong> {application.notes}
</p>
)}
</div>
<div className="ml-4">
<button
onClick={() => handleViewApplication(application)}
className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View details"
>
<EyeIcon className="h-5 w-5" />
</button>
</div>
</div>
</div>
);
}
// Mowing entry
const log = item.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 (
<div key={`mow-${log.id}`} className="bg-white p-6 rounded-lg shadow hover:shadow-lg transition-shadow">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900">{log.property_name || 'Property'}</div>
<div className="text-sm text-gray-600">{new Date(item.date).toLocaleString()} {log.equipment_name || 'Mower'} {avg} mph {distFeet} ft {durationMin} min</div>
</div>
<button
onClick={() => setViewingMowingSession(log)}
className="p-2 text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded"
title="View mowing session"
>
<EyeIcon className="h-5 w-5" />
</button>
</div>
</div>
);
})}
</div>
)}
{/* Application View Modal */}
{showViewModal && viewingApplication && (
<ApplicationViewModal
application={viewingApplication}
propertyDetails={selectedPropertyDetails}
onClose={() => {
setShowViewModal(false);
setViewingApplication(null);
}}
/>
)}
{/* Mowing Session View Modal */}
{viewingMowingSession && (
<MowingSessionViewModal
session={viewingMowingSession}
onClose={() => setViewingMowingSession(null)}
/>
)}
</div>
);
};
export default History;