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 (
Total Applications
{totalApplications}
Total Area Treated
{Math.round(totalAreaTreated / 1000)}k sq ft
Total Time
{Math.round(totalDuration / 3600)}h
Try adjusting your filters or date range.
Notes: {application.notes}
)}