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);
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) => {
try {
const response = await propertiesAPI.getById(parseInt(propertyId));
const property = response.data.data.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 (
);
}
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 && (
Calculated Requirements:
• 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.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),
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
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
},
nozzle: selectedNozzle ? {
id: selectedNozzle.id,
flowRateGpm: selectedNozzle.flowRateGpm,
sprayAngle: selectedNozzle.sprayAngle
} : null,
products: [{
...(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),
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
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
},
nozzle: selectedNozzle ? {
id: selectedNozzle.id,
flowRateGpm: selectedNozzle.flowRateGpm,
sprayAngle: selectedNozzle.sprayAngle
} : null,
products: [{
...(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,
applicationType: '', // 'liquid' or 'granular'
equipmentId: '',
nozzleId: '',
plannedDate: '',
notes: ''
});
// Initialize form with editing data
useEffect(() => {
if (editingPlan) {
const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id;
if (propertyId && propertyId !== planData.propertyId) {
onPropertySelect(propertyId);
}
// 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 || ''
});
}
}, [editingPlan, products, onPropertySelect]);
const handlePropertyChange = async (propertyId) => {
setPlanData({ ...planData, propertyId, selectedAreas: [] });
if (propertyId) {
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.productId) {
toast.error('Please select a product');
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'}
);
};
export default Applications;