import React, { useState, useEffect } from 'react'; import { PlusIcon, MapPinIcon, BeakerIcon, WrenchScrewdriverIcon, CalculatorIcon, PencilIcon, TrashIcon, PlayIcon, EyeIcon, ArchiveBoxIcon, FunnelIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; import PropertyMap from '../../components/Maps/PropertyMap'; import ApplicationExecutionModal from '../../components/Applications/ApplicationExecutionModal'; import ApplicationPlanModal from '../../components/Applications/ApplicationPlanModal'; import ApplicationViewModal from '../../components/Applications/ApplicationViewModal'; import toast from 'react-hot-toast'; const Applications = () => { const [showPlanForm, setShowPlanForm] = useState(false); const [applications, setApplications] = useState([]); const [loading, setLoading] = useState(true); const [properties, setProperties] = useState([]); const [products, setProducts] = useState([]); const [equipment, setEquipment] = useState([]); const [nozzles, setNozzles] = useState([]); const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null); const [editingPlan, setEditingPlan] = useState(null); const [propertyCache, setPropertyCache] = useState({}); const [spreaderRecommendation, setSpreaderRecommendation] = useState(null); const [loadingRecommendation, setLoadingRecommendation] = useState(false); const [executingApplication, setExecutingApplication] = useState(null); const [showExecutionModal, setShowExecutionModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); const [viewingApplication, setViewingApplication] = useState(null); // Filtering and sorting state const [showFilters, setShowFilters] = useState(false); const [filters, setFilters] = useState({ status: 'all', dateRange: 'all', product: 'all', minArea: '', maxArea: '', property: 'all' }); const [sortBy, setSortBy] = useState('date'); const [sortOrder, setSortOrder] = useState('desc'); useEffect(() => { fetchApplications(); fetchPlanningData(); }, []); const fetchApplications = async () => { try { const response = await applicationsAPI.getPlans(); setApplications(response.data.data.plans || []); } catch (error) { console.error('Failed to fetch applications:', error); toast.error('Failed to load applications'); } }; const fetchPlanningData = async () => { try { setLoading(true); const [propertiesResponse, productsResponse, equipmentResponse, nozzlesResponse] = await Promise.all([ propertiesAPI.getAll(), productsAPI.getAll(), equipmentAPI.getAll(), nozzlesAPI.getAll() ]); setProperties(propertiesResponse.data.data.properties || []); // Combine shared and user products with unique IDs const sharedProducts = (productsResponse.data.data.sharedProducts || []).map(product => ({ ...product, uniqueId: `shared_${product.id}`, isShared: true })); const userProducts = (productsResponse.data.data.userProducts || []).map(product => ({ ...product, uniqueId: `user_${product.id}`, isShared: false })); const allProducts = [...sharedProducts, ...userProducts]; setProducts(allProducts); setEquipment(equipmentResponse.data.data.equipment || []); setNozzles(nozzlesResponse.data.data?.nozzles || nozzlesResponse.data || []); } catch (error) { console.error('Failed to fetch planning data:', error); toast.error('Failed to load planning data'); } finally { setLoading(false); } }; const fetchPropertyDetails = async (propertyId) => { // Validate propertyId if (!propertyId || isNaN(parseInt(propertyId))) { console.error('Invalid property ID:', propertyId); toast.error('Invalid property ID'); setSelectedPropertyDetails(null); return null; } // Check cache first if (propertyCache[propertyId]) { setSelectedPropertyDetails(propertyCache[propertyId]); return propertyCache[propertyId]; } try { const response = await propertiesAPI.getById(parseInt(propertyId)); const property = response.data.data.property; // Cache the result setPropertyCache(prev => ({ ...prev, [propertyId]: property })); setSelectedPropertyDetails(property); return property; } catch (error) { console.error('Failed to fetch property details:', error); toast.error('Failed to load property details'); setSelectedPropertyDetails(null); return null; } }; const handleDeletePlan = async (planId, planName) => { if (window.confirm(`Are you sure you want to delete the plan for "${planName}"?`)) { try { await applicationsAPI.deletePlan(planId); toast.success('Application plan deleted successfully'); fetchApplications(); // Refresh the list } catch (error) { console.error('Failed to delete plan:', error); toast.error('Failed to delete application plan'); } } }; const handleEditPlan = async (planId) => { try { // Fetch the full plan details const response = await applicationsAPI.getPlan(planId); const plan = response.data.data.plan; // Set up the editing plan data setEditingPlan(plan); setShowPlanForm(true); } catch (error) { console.error('Failed to fetch plan details:', error); toast.error('Failed to load plan details'); } }; const handleExecuteApplication = async (application) => { try { // Set the executing application and show the modal setExecutingApplication(application); // Get the property ID from the application const propertyId = application.property?.id || application.section?.propertyId; // Try to fetch property details if we have a valid property ID if (propertyId && (!selectedPropertyDetails || selectedPropertyDetails.id !== propertyId)) { await fetchPropertyDetails(propertyId); } else if (!propertyId) { console.warn('No property ID found for application:', application); // Clear any existing property details since this application doesn't have property info setSelectedPropertyDetails(null); } setShowExecutionModal(true); } catch (error) { console.error('Failed to start application execution:', error); toast.error('Failed to start application execution'); } }; const handleViewApplication = async (application) => { try { // Set the viewing application and show the modal setViewingApplication(application); // Get the property ID from the application const propertyId = application.property?.id || application.section?.propertyId; // Try to fetch property details if we have a valid property ID if (propertyId && (!selectedPropertyDetails || selectedPropertyDetails.id !== propertyId)) { await fetchPropertyDetails(propertyId); } else if (!propertyId) { console.warn('No property ID found for application:', application); // Clear any existing property details since this application doesn't have property info setSelectedPropertyDetails(null); } setShowViewModal(true); } catch (error) { console.error('Failed to load application details:', error); toast.error('Failed to load application details'); } }; const handleArchiveApplication = async (applicationId) => { if (!window.confirm('Are you sure you want to archive this application? It will be moved to the archive and hidden from the main list.')) { return; } try { await applicationsAPI.updatePlanStatus(applicationId, 'archived'); toast.success('Application archived successfully'); fetchApplications(); // Refresh the list } catch (error) { console.error('Failed to archive application:', error); toast.error('Failed to archive application'); } }; // Filter and sort applications const filteredAndSortedApplications = React.useMemo(() => { let filtered = applications.filter(app => { // Hide archived applications unless specifically filtering for them if (app.status === 'archived' && filters.status !== 'archived') { return false; } // Status filter if (filters.status !== 'all' && app.status !== filters.status) { return false; } // Date range filter if (filters.dateRange !== 'all') { const appDate = new Date(app.plannedDate); const now = new Date(); switch (filters.dateRange) { 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; } } // Area filter const area = app.totalSectionArea || app.sectionArea || 0; if (filters.minArea && area < parseInt(filters.minArea)) { return false; } if (filters.maxArea && area > parseInt(filters.maxArea)) { return false; } // Property filter if (filters.property !== 'all' && app.propertyName !== filters.property) { return false; } // Product filter if (filters.product !== 'all') { const hasProduct = app.productDetails?.some(p => p.name.toLowerCase().includes(filters.product.toLowerCase())) || app.products?.some(p => p.productName?.toLowerCase().includes(filters.product.toLowerCase())); if (!hasProduct) return false; } return true; }); // Sort return filtered.sort((a, b) => { let aVal, bVal; switch (sortBy) { case 'date': aVal = new Date(a.plannedDate); bVal = new Date(b.plannedDate); break; case 'area': aVal = a.totalSectionArea || a.sectionArea || 0; bVal = b.totalSectionArea || b.sectionArea || 0; break; case 'property': aVal = a.propertyName || ''; bVal = b.propertyName || ''; break; case 'status': aVal = a.status || ''; bVal = b.status || ''; break; default: return 0; } if (sortOrder === 'desc') { return bVal > aVal ? 1 : bVal < aVal ? -1 : 0; } else { return aVal > bVal ? 1 : aVal < bVal ? -1 : 0; } }); }, [applications, filters, sortBy, sortOrder]); // Get unique values for filter dropdowns const uniqueProperties = React.useMemo(() => { const props = [...new Set(applications.map(app => app.propertyName).filter(Boolean))]; return props.sort(); }, [applications]); const uniqueProducts = React.useMemo(() => { const products = new Set(); applications.forEach(app => { app.productDetails?.forEach(p => products.add(p.name)); app.products?.forEach(p => p.productName && products.add(p.productName)); }); return [...products].sort(); }, [applications]); if (loading) { return (
Plan, track, and log your lawn applications
Start by planning your first lawn application
Areas: {application.sectionNames} ({Math.round(application.totalSectionArea || application.sectionArea || 0).toLocaleString()} sq ft)
Equipment: {application.equipmentName}
Products: {application.productCount}
{/* Display calculated amounts */} {(application.totalProductAmount > 0 || (application.productDetails && application.productDetails.length > 0)) && (Calculated Requirements:
{/* Show individual products for liquid tank mix */} {application.productDetails && application.productDetails.length > 1 ? ( <> {application.productDetails.map((product, index) => (• {product.name}{product.brand ? ` (${product.brand})` : ''}: {product.calculatedAmount.toFixed(2)} oz
))} {application.totalWaterAmount > 0 && (• Water: {application.totalWaterAmount.toFixed(2)} gallons
)} {application.avgSpeedMph > 0 && (• Target Speed: {application.avgSpeedMph.toFixed(1)} mph
)} {application.spreaderSetting && (• Spreader Setting: {application.spreaderSetting}
)} > ) : ( <> {/* Show single product with name */} {application.productDetails && application.productDetails.length === 1 ? (• {application.productDetails[0].name}{application.productDetails[0].brand ? ` (${application.productDetails[0].brand})` : ''}: {application.productDetails[0].calculatedAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}
) : (• Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}
)} {application.totalWaterAmount > 0 && (• Water: {application.totalWaterAmount.toFixed(2)} gallons
)} {application.avgSpeedMph > 0 && (• Target Speed: {application.avgSpeedMph.toFixed(1)} mph
)} {application.spreaderSetting && (• Spreader Setting: {application.spreaderSetting}
)} > )}"{application.notes}"
)}{application.plannedDate ? new Date(application.plannedDate).toLocaleDateString() : 'No date set'}
Created {new Date(application.createdAt).toLocaleDateString()}