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 hidden from the main list but preserved in your records.')) { return; } try { // Try different possible status values that might work for archiving const possibleArchiveStatuses = ['archived', 'hidden', 'inactive', 'done']; for (const status of possibleArchiveStatuses) { try { await applicationsAPI.updatePlanStatus(applicationId, status); toast.success('Application archived successfully'); fetchApplications(); // Refresh the list return; } catch (statusError) { // Continue to next status if this one fails console.log(`Status '${status}' failed, trying next...`); } } // If all status updates fail, we need to use a different approach // Let's try updating the plan with a custom archived flag const planResponse = await applicationsAPI.getPlan(applicationId); const planData = planResponse.data.data.plan; // Update the plan with an archived flag await applicationsAPI.updatePlan(applicationId, { ...planData, archived: true, notes: planData.notes ? `${planData.notes} [ARCHIVED]` : '[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 viewing archived ones const isArchived = app.archived === true || app.notes?.includes('[ARCHIVED]') || ['archived', 'hidden', 'inactive', 'done'].includes(app.status); if (isArchived && filters.status !== 'archived') { return false; } // If filtering for archived, only show archived items if (filters.status === 'archived' && !isArchived) { return false; } // Status filter (excluding archived since we handle it separately) if (filters.status !== 'all' && filters.status !== 'archived' && 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 (
); } // Load spreader recommendations when granular product and spreader are selected const loadSpreaderRecommendation = async (product, equipmentId, selectedAreas) => { if (!product || !equipmentId || !selectedAreas.length || product.productType !== 'granular') { setSpreaderRecommendation(null); return; } setLoadingRecommendation(true); try { // Find the selected equipment details const selectedEquipment = equipment.find(eq => eq.id === parseInt(equipmentId)); if (!selectedEquipment) { setSpreaderRecommendation(null); return; } // Load spreader settings for this product const productApiId = product.isShared ? product.id : product.id; // Use the actual product ID const endpoint = product.isShared ? `/api/product-spreader-settings/product/${productApiId}` : `/api/product-spreader-settings/user-product/${productApiId}`; const response = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } }); if (!response.ok) { setSpreaderRecommendation(null); return; } const data = await response.json(); const settings = data.data?.settings || []; // Find a matching setting for this equipment let matchingSetting = null; // First try to find exact equipment match matchingSetting = settings.find(setting => setting.equipmentId === selectedEquipment.id ); // If no exact match, try to find by equipment brand/manufacturer if (!matchingSetting && selectedEquipment.manufacturer) { matchingSetting = settings.find(setting => setting.spreaderBrand && setting.spreaderBrand.toLowerCase().includes(selectedEquipment.manufacturer.toLowerCase()) ); } // If still no match, use any available setting as fallback if (!matchingSetting && settings.length > 0) { matchingSetting = settings[0]; } if (matchingSetting) { // Calculate total area and product amount needed const totalArea = selectedAreas.reduce((sum, areaId) => { const area = selectedPropertyDetails?.sections?.find(s => s.id === areaId); return sum + (area?.area || 0); }, 0); // Calculate product amount based on rate const rateAmount = product.customRateAmount || product.rateAmount || 1; const rateUnit = product.customRateUnit || product.rateUnit || 'lbs/1000 sq ft'; let productAmountLbs = 0; if (rateUnit.includes('1000')) { // Rate per 1000 sq ft productAmountLbs = (rateAmount * totalArea) / 1000; } else if (rateUnit.includes('acre')) { // Rate per acre (43,560 sq ft) productAmountLbs = (rateAmount * totalArea) / 43560; } else { // Assume rate per sq ft productAmountLbs = rateAmount * totalArea; } setSpreaderRecommendation({ setting: matchingSetting, equipment: selectedEquipment, totalArea, productAmountLbs: Math.round(productAmountLbs * 100) / 100, // Round to 2 decimal places isExactMatch: settings.some(s => s.equipmentId === selectedEquipment.id) }); } else { setSpreaderRecommendation(null); } } catch (error) { console.error('Failed to load spreader recommendation:', error); setSpreaderRecommendation(null); } finally { setLoadingRecommendation(false); } }; return (

Applications

Plan, track, and log your lawn applications

{/* Filter Controls */} {applications.length > 0 && (
{filteredAndSortedApplications.length} of {applications.length} applications
{showFilters && (
{/* Status Filter */}
{/* Date Range Filter */}
{/* Property Filter */}
{/* Sort By */}
{/* Area Range Filter */}
setFilters(prev => ({ ...prev, minArea: e.target.value }))} placeholder="Min" className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm" /> to setFilters(prev => ({ ...prev, maxArea: e.target.value }))} placeholder="Max" className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm" />
{/* Product Filter */}
setFilters(prev => ({ ...prev, product: e.target.value }))} placeholder="Search products..." className="flex-1 border border-gray-300 rounded px-3 py-2 text-sm" /> {filters.product !== 'all' && filters.product !== '' && ( )}
{/* Clear Filters Button */}
)}
)} {/* Applications List */} {applications.length === 0 ? (

No Applications Yet

Start by planning your first lawn application

) : (
{filteredAndSortedApplications.map((application) => (

{application.propertyName} - {application.sectionName}

{application.status}

{application.propertyAddress}

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.notes}"

)}

{application.plannedDate ? new Date(application.plannedDate).toLocaleDateString() : 'No date set'}

Created {new Date(application.createdAt).toLocaleDateString()}

{application.status === 'planned' && ( <> )} {application.status === 'completed' && ( <> )}
))}
)} {/* Plan Application Modal */} {showPlanForm && ( { setShowPlanForm(false); setEditingPlan(null); setSelectedPropertyDetails(null); }} properties={properties} products={products} equipment={equipment} nozzles={nozzles} selectedPropertyDetails={selectedPropertyDetails} onPropertySelect={fetchPropertyDetails} editingPlan={editingPlan} onSubmit={async (planData) => { try { if (editingPlan) { // Edit existing plan - backend now supports multiple areas natively const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; // Calculate total area for all selected areas const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => { const area = selectedPropertyDetails.sections.find(s => s.id === areaId); return total + (area?.area || 0); }, 0); const planPayload = { lawnSectionIds: planData.selectedAreas.map(id => parseInt(id)), // Send multiple areas equipmentId: parseInt(planData.equipmentId), ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], notes: planData.notes || '', areaSquareFeet: totalAreaSquareFeet, equipment: { id: selectedEquipment?.id, categoryName: selectedEquipment?.categoryName, tankSizeGallons: selectedEquipment?.tankSizeGallons, pumpGpm: selectedEquipment?.pumpGpm, sprayWidthFeet: selectedEquipment?.sprayWidthFeet, capacityLbs: selectedEquipment?.capacityLbs, spreadWidth: selectedEquipment?.spreadWidth }, ...(planData.applicationType === 'liquid' && selectedNozzle && { nozzle: { id: selectedNozzle.id, flowRateGpm: selectedNozzle.flowRateGpm, sprayAngle: selectedNozzle.sprayAngle } }), products: planData.applicationType === 'liquid' ? planData.selectedProducts.map(item => ({ ...(item.product?.isShared ? { productId: parseInt(item.product.id) } : { userProductId: parseInt(item.product.id) } ), rateAmount: parseFloat(item.rateAmount || 1), rateUnit: item.rateUnit || 'oz/1000 sq ft', applicationType: planData.applicationType })) : [{ ...(planData.selectedProduct?.isShared ? { productId: parseInt(planData.selectedProduct.id) } : { userProductId: parseInt(planData.selectedProduct.id) } ), rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1), rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft', applicationType: planData.applicationType }] }; await applicationsAPI.updatePlan(editingPlan.id, planPayload); toast.success(`Application plan updated successfully for ${planData.selectedAreas.length} area(s)`); } else { // Create new plan - backend now supports multiple areas in single plan const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; // Calculate total area for all selected areas const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => { const area = selectedPropertyDetails.sections.find(s => s.id === areaId); return total + (area?.area || 0); }, 0); const planPayload = { lawnSectionIds: planData.selectedAreas.map(id => parseInt(id)), // Send multiple areas equipmentId: parseInt(planData.equipmentId), ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: new Date().toISOString().split('T')[0], notes: planData.notes || '', areaSquareFeet: totalAreaSquareFeet, equipment: { id: selectedEquipment?.id, categoryName: selectedEquipment?.categoryName, tankSizeGallons: selectedEquipment?.tankSizeGallons, pumpGpm: selectedEquipment?.pumpGpm, sprayWidthFeet: selectedEquipment?.sprayWidthFeet, capacityLbs: selectedEquipment?.capacityLbs, spreadWidth: selectedEquipment?.spreadWidth }, ...(planData.applicationType === 'liquid' && selectedNozzle && { nozzle: { id: selectedNozzle.id, flowRateGpm: selectedNozzle.flowRateGpm, sprayAngle: selectedNozzle.sprayAngle } }), products: planData.applicationType === 'liquid' ? planData.selectedProducts.map(item => ({ ...(item.product?.isShared ? { productId: parseInt(item.product.id) } : { userProductId: parseInt(item.product.id) } ), rateAmount: parseFloat(item.rateAmount || 1), rateUnit: item.rateUnit || 'oz/1000 sq ft', applicationType: planData.applicationType })) : [{ ...(planData.selectedProduct?.isShared ? { productId: parseInt(planData.selectedProduct.id) } : { userProductId: parseInt(planData.selectedProduct.id) } ), rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1), rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft', applicationType: planData.applicationType }] }; await applicationsAPI.createPlan(planPayload); toast.success(`Created application plan for ${planData.selectedAreas.length} area(s) successfully`); } setShowPlanForm(false); setEditingPlan(null); fetchApplications(); } catch (error) { console.error('Failed to save application plan:', error); toast.error('Failed to save application plan'); } }} /> )} {/* Application Execution Modal */} {showExecutionModal && executingApplication && ( { setShowExecutionModal(false); setExecutingApplication(null); }} onComplete={() => { fetchApplications(); setShowExecutionModal(false); setExecutingApplication(null); }} /> )} {/* Application View Modal */} {showViewModal && viewingApplication && ( { setShowViewModal(false); setViewingApplication(null); }} /> )}
); }; export default Applications;