import React, { useState, useEffect } from 'react'; import { PlusIcon, MapPinIcon, BeakerIcon, WrenchScrewdriverIcon, CalculatorIcon, PencilIcon, TrashIcon } 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 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); 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) => { // 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'); } }; 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

{/* Applications List */} {applications.length === 0 ? (

No Applications Yet

Start by planning your first lawn application

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

{application.propertyName} - {application.sectionName}

{application.status}

{application.propertyAddress}

Area: {Math.round(application.sectionArea).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

)} ) : ( <>

• 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()}

))}
)} {/* Plan Application Modal */} {showPlanForm && ( { setShowPlanForm(false); setEditingPlan(null); }} properties={properties} products={products} equipment={equipment} nozzles={nozzles} selectedPropertyDetails={selectedPropertyDetails} onPropertySelect={fetchPropertyDetails} editingPlan={editingPlan} onSubmit={async (planData) => { try { if (editingPlan) { // Edit existing plan const selectedArea = selectedPropertyDetails.sections.find(s => s.id === planData.selectedAreas[0]); const areaSquareFeet = selectedArea?.area || 0; const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; const planPayload = { lawnSectionId: parseInt(planData.selectedAreas[0]), 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: areaSquareFeet, 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'); } else { // Create new plan(s) const planPromises = planData.selectedAreas.map(async (areaId) => { const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId); const areaSquareFeet = selectedArea?.area || 0; const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; const planPayload = { lawnSectionId: parseInt(areaId), equipmentId: parseInt(planData.equipmentId), ...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }), plannedDate: new Date().toISOString().split('T')[0], notes: planData.notes || '', areaSquareFeet: areaSquareFeet, 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 }] }; return applicationsAPI.createPlan(planPayload); }); await Promise.all(planPromises); toast.success(`Created ${planData.selectedAreas.length} application plan(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 Planning Modal Component const ApplicationPlanModal = ({ onClose, onSubmit, properties, products, equipment, nozzles, selectedPropertyDetails, onPropertySelect, editingPlan }) => { const [loadingProperty, setLoadingProperty] = useState(false); const [planData, setPlanData] = useState({ propertyId: '', selectedAreas: [], productId: '', selectedProduct: null, selectedProducts: [], // For liquid tank mixing - array of {product, rate} applicationType: '', // 'liquid' or 'granular' equipmentId: '', nozzleId: '', plannedDate: '', notes: '' }); // Initialize form with editing data useEffect(() => { if (editingPlan && products.length > 0) { const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id; // Find the product from the plans products array const planProduct = editingPlan.products?.[0]; let selectedProduct = null; if (planProduct) { if (planProduct.productId) { selectedProduct = products.find(p => p.uniqueId === `shared_${planProduct.productId}`); } else if (planProduct.userProductId) { selectedProduct = products.find(p => p.uniqueId === `user_${planProduct.userProductId}`); } } setPlanData({ propertyId: propertyId?.toString() || '', selectedAreas: [editingPlan.section?.id], productId: selectedProduct?.uniqueId || '', selectedProduct: selectedProduct, applicationType: planProduct?.applicationType || '', equipmentId: editingPlan.equipment?.id?.toString() || '', nozzleId: editingPlan.nozzle?.id?.toString() || '', plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '', notes: editingPlan.notes || '' }); // Only fetch property details if we don't already have them if (propertyId && (!selectedPropertyDetails || selectedPropertyDetails.id !== propertyId)) { onPropertySelect(propertyId); } } }, [editingPlan, products]); const handlePropertyChange = async (propertyId) => { setPlanData({ ...planData, propertyId, selectedAreas: [] }); if (propertyId && propertyId !== selectedPropertyDetails?.id?.toString()) { setLoadingProperty(true); await onPropertySelect(propertyId); setLoadingProperty(false); } }; // Filter equipment based on application type const availableEquipment = equipment.filter(eq => { if (planData.applicationType === 'liquid') { return eq.categoryName === 'Sprayer'; } else if (planData.applicationType === 'granular') { return eq.categoryName === 'Spreader'; } return false; }); const handleSubmit = (e) => { e.preventDefault(); if (!planData.propertyId || planData.selectedAreas.length === 0) { toast.error('Please select a property and at least one area'); return; } if (!planData.applicationType) { toast.error('Please select an application type'); return; } // Validate product selection based on application type if (planData.applicationType === 'granular') { if (!planData.productId) { toast.error('Please select a product'); return; } } else if (planData.applicationType === 'liquid') { if (planData.selectedProducts.length === 0) { toast.error('Please select at least one product for tank mixing'); return; } // Validate that all selected products have rates const missingRates = planData.selectedProducts.filter(p => !p.rateAmount || p.rateAmount <= 0); if (missingRates.length > 0) { toast.error('Please enter application rates for all selected products'); return; } } if (!planData.equipmentId) { toast.error('Please select equipment'); return; } onSubmit(planData); }; const handleAreaToggle = (areaId) => { setPlanData(prev => ({ ...prev, selectedAreas: prev.selectedAreas.includes(areaId) ? prev.selectedAreas.filter(id => id !== areaId) : [...prev.selectedAreas, areaId] })); }; return (

{editingPlan ? 'Edit Application Plan' : 'Plan Application'}

{/* Property Selection */}
{/* Area Selection with Map */} {loadingProperty && (
Loading property details...
)} {selectedPropertyDetails && (
{/* Property Map */} {selectedPropertyDetails.latitude && selectedPropertyDetails.longitude && (
handleAreaToggle(section.id)} />

Click sections to select

{planData.selectedAreas.length > 0 && (

{planData.selectedAreas.length} selected

)}
)} {selectedPropertyDetails.sections && selectedPropertyDetails.sections.length > 0 ? (
{selectedPropertyDetails.sections.map((section) => ( ))}
{planData.selectedAreas.length > 0 && (

Total area: {selectedPropertyDetails.sections .filter(s => planData.selectedAreas.includes(s.id)) .reduce((total, s) => total + (s.area || 0), 0).toFixed(0)} sq ft

)}
) : (

This property has no lawn sections defined. Please add lawn sections to the property first.

)}
)} {/* Application Type Selection */}
{/* Product Selection - Single for Granular */} {planData.applicationType === 'granular' && (
)} {/* Product Selection - Multiple for Liquid Tank Mix */} {planData.applicationType === 'liquid' && (
{/* Selected Products List */} {planData.selectedProducts.length > 0 && (
{planData.selectedProducts.map((item, index) => (
{item.product.customName || item.product.name} {item.product.brand || item.product.customBrand ? ( - {item.product.brand || item.product.customBrand} ) : null}
{ const newProducts = [...planData.selectedProducts]; newProducts[index] = { ...item, rateAmount: parseFloat(e.target.value) || 0 }; setPlanData({ ...planData, selectedProducts: newProducts }); }} className="w-20 px-2 py-1 text-sm border rounded" placeholder="Rate" />
))}
)} {/* Add Product Dropdown */} {planData.selectedProducts.length === 0 && (

Select liquid products to mix in the tank. You can add herbicides, surfactants, and other liquid products.

)}
)} {/* Equipment Selection */} {planData.applicationType && (
{availableEquipment.length === 0 && (

No {planData.applicationType === 'liquid' ? 'sprayers' : 'spreaders'} found. Please add equipment first.

)}
)} {/* Spreader Recommendation for Granular Applications */} {/* Nozzle Selection for Liquid Applications */} {planData.applicationType === 'liquid' && (
{nozzles.length === 0 && (

No nozzles found. Go to Equipment → Add Equipment → Select "Nozzle" category to add nozzles first.

)}
)} {/* Planned Date */}
setPlanData({ ...planData, plannedDate: e.target.value })} />
{/* Notes */}