import React, { useState, useEffect } from 'react'; import { XMarkIcon, MapPinIcon, WrenchScrewdriverIcon, BeakerIcon } from '@heroicons/react/24/outline'; import PropertyMap from '../Maps/PropertyMap'; import toast from 'react-hot-toast'; const ApplicationPlanModal = ({ onClose, properties, products, equipment, nozzles, selectedPropertyDetails, onPropertySelect, editingPlan, onSubmit }) => { const [selectedPropertyId, setSelectedPropertyId] = useState(''); const [selectedAreas, setSelectedAreas] = useState([]); const [selectedEquipmentId, setSelectedEquipmentId] = useState(''); const [selectedNozzleId, setSelectedNozzleId] = useState(''); const [selectedProducts, setSelectedProducts] = useState([]); const [applicationType, setApplicationType] = useState(''); // Seed-specific scenario const [seedMode, setSeedMode] = useState('overseed'); const [plannedDate, setPlannedDate] = useState(new Date().toISOString().split('T')[0]); const [notes, setNotes] = useState(''); const [loading, setLoading] = useState(false); const [spreaderSettings, setSpreaderSettings] = useState({}); const [showSpreaderSettingsForm, setShowSpreaderSettingsForm] = useState(false); const [currentProductForSettings, setCurrentProductForSettings] = useState(null); const [spreaderFormData, setSpreaderFormData] = useState({ setting: '', rateDescription: '', notes: '' }); // Calculate map center from property or sections const mapCenter = React.useMemo(() => { // First try to use property coordinates if (selectedPropertyDetails?.latitude && selectedPropertyDetails?.longitude) { return [selectedPropertyDetails.latitude, selectedPropertyDetails.longitude]; } // Fall back to calculating center from sections if (selectedPropertyDetails?.sections?.length > 0) { let totalLat = 0; let totalLng = 0; let pointCount = 0; selectedPropertyDetails.sections.forEach(section => { let polygonData = section.polygonData; if (typeof polygonData === 'string') { try { polygonData = JSON.parse(polygonData); } catch (e) { return; } } if (polygonData?.coordinates?.[0]) { polygonData.coordinates[0].forEach(([lat, lng]) => { totalLat += lat; totalLng += lng; pointCount++; }); } }); if (pointCount > 0) { return [totalLat / pointCount, totalLng / pointCount]; } } // Default center return [39.8283, -98.5795]; }, [selectedPropertyDetails]); // Initialize form with editing data if provided useEffect(() => { if (!editingPlan) return; // Property and sections const propId = editingPlan.property?.id || editingPlan.propertyId; if (propId) { setSelectedPropertyId(String(propId)); if (onPropertySelect) onPropertySelect(propId); } const areaIds = (editingPlan.sections || editingPlan.selectedAreas || []).map(s => (typeof s === 'object' ? s.id : s)); setSelectedAreas(areaIds); // Equipment/nozzle if (editingPlan.equipment?.id || editingPlan.equipmentId) setSelectedEquipmentId(String(editingPlan.equipment?.id || editingPlan.equipmentId)); if (editingPlan.nozzle?.id || editingPlan.nozzleId) setSelectedNozzleId(String(editingPlan.nozzle?.id || editingPlan.nozzleId)); // Date/notes setPlannedDate(editingPlan.plannedDate || new Date().toISOString().split('T')[0]); setNotes(editingPlan.notes || ''); // Determine application type const ptypes = (editingPlan.products || []).map(p => (p.productType || '').toLowerCase()); const derivedType = ptypes.includes('liquid') ? 'liquid' : (ptypes.includes('seed') ? 'seed' : 'granular'); setApplicationType(derivedType); if (derivedType === 'seed') setSeedMode('overseed'); // Map products into modal structure const mapped = (editingPlan.products || []).map(p => ({ uniqueId: p.userProductId ? `user_${p.userProductId}` : `shared_${p.productId}`, productId: p.productId || null, userProductId: p.userProductId || null, productName: p.productName, productBrand: p.productBrand, productType: p.productType, rateAmount: p.rateAmount, rateUnit: p.rateUnit, isUserProduct: !!p.userProductId })); setSelectedProducts(mapped); }, [editingPlan, onPropertySelect]); // Handle property selection const handlePropertyChange = async (propertyId) => { setSelectedPropertyId(propertyId); setSelectedAreas([]); // Clear selected areas when property changes if (propertyId && onPropertySelect) { await onPropertySelect(propertyId); } }; // Debug logging React.useEffect(() => { console.log('=== ApplicationPlanModal Debug ==='); console.log('selectedPropertyDetails:', selectedPropertyDetails); console.log('mapCenter:', mapCenter); console.log('sections:', selectedPropertyDetails?.sections); console.log('\n=== EQUIPMENT DEBUG ==='); console.log('equipment array length:', equipment?.length); console.log('equipment full array:', equipment); if (equipment && equipment.length > 0) { console.log('first equipment item:', equipment[0]); console.log('equipment field analysis:'); equipment.forEach((eq, i) => { console.log(`Equipment ${i}:`, { id: eq.id, name: eq.name, equipment_name: eq.equipment_name, custom_name: eq.custom_name, category_name: eq.category_name, type_name: eq.type_name, category: eq.category, type: eq.type, allFields: Object.keys(eq) }); }); } console.log('\n=== PRODUCTS DEBUG ==='); console.log('products array length:', products?.length); console.log('products full array:', products); if (products && products.length > 0) { console.log('first product item:', products[0]); console.log('product field analysis:'); products.forEach((prod, i) => { if (i < 3) { // Only show first 3 to avoid spam console.log(`Product ${i}:`, { id: prod.id, name: prod.name, product_name: prod.product_name, productName: prod.productName, brand: prod.brand, product_brand: prod.product_brand, productBrand: prod.productBrand, productType: prod.productType, product_type: prod.product_type, type: prod.type, isShared: prod.isShared, uniqueId: prod.uniqueId, allFields: Object.keys(prod) }); } }); } console.log('\n=== NOZZLES DEBUG ==='); console.log('nozzles array length:', nozzles?.length); console.log('nozzles full array:', nozzles); if (nozzles && nozzles.length > 0) { console.log('first nozzle item:', nozzles[0]); console.log('nozzle field analysis:'); nozzles.forEach((nozzle, i) => { console.log(`Nozzle ${i}:`, { id: nozzle.id, name: nozzle.name, custom_name: nozzle.custom_name, manufacturer: nozzle.manufacturer, flow_rate_gpm: nozzle.flow_rate_gpm, orifice_size: nozzle.orifice_size, allFields: Object.keys(nozzle) }); }); } console.log('=== End Debug ===\n'); }, [selectedPropertyDetails, mapCenter, equipment, products, nozzles]); // Handle area selection on map const handleAreaClick = (area) => { setSelectedAreas(prev => { if (prev.includes(area.id)) { return prev.filter(id => id !== area.id); } else { return [...prev, area.id]; } }); }; // Add product to plan (for liquid tank mixes) const addProduct = () => { setSelectedProducts(prev => [...prev, { uniqueId: '', productId: null, userProductId: null, productName: '', productBrand: '', productType: applicationType, rateAmount: '', rateUnit: (applicationType === 'granular' || applicationType === 'seed') ? 'lb/1000sqft' : 'oz/1000sqft', isUserProduct: false }]); }; // Handle application type change const handleApplicationTypeChange = (type) => { setApplicationType(type); if (type === 'seed') setSeedMode('overseed'); // Clear products when switching type setSelectedProducts([]); // Add initial product setTimeout(() => { setSelectedProducts([{ uniqueId: '', productId: null, userProductId: null, productName: '', productBrand: '', productType: type, rateAmount: '', rateUnit: (type === 'granular' || type === 'seed') ? 'lb/1000sqft' : 'oz/1000sqft', isUserProduct: false }]); }, 0); }; // Remove product from plan const removeProduct = (index) => { setSelectedProducts(prev => prev.filter((_, i) => i !== index)); }; // Update product in plan const updateProduct = (index, field, value) => { setSelectedProducts(prev => prev.map((product, i) => i === index ? { ...product, [field]: value } : product )); }; // Check spreader settings for granular products const checkSpreaderSettings = async (product, equipmentId) => { try { const productApiId = product.isShared ? product.id : product.id; const endpoint = product.isShared ? `/api/product-spreader-settings/product/${productApiId}` : `/api/product-spreader-settings/user-product/${productApiId}`; console.log('Checking spreader settings:', { product, equipmentId, endpoint }); const response = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } }); if (response.ok) { const data = await response.json(); console.log('Spreader settings response:', data); const settings = data.data?.settings || []; // For shared products, check if any setting has the matching equipmentId // For user products, the query already filters by equipment const equipmentSetting = settings.find(s => { console.log('Comparing setting:', s, 'with equipmentId:', equipmentId); return s.equipmentId === parseInt(equipmentId); }); console.log('Found equipment setting:', equipmentSetting); if (!equipmentSetting) { // No spreader setting found, prompt user to add one console.log('No setting found, showing form'); setCurrentProductForSettings(product); setShowSpreaderSettingsForm(true); } else { // Store the spreader setting for calculations console.log('Setting found, storing for calculations'); setSpreaderSettings(prev => ({ ...prev, [`${product.id}_${equipmentId}`]: equipmentSetting })); } } else { console.log('Response not ok:', response.status); } } catch (error) { console.error('Failed to check spreader settings:', error); } }; // Save spreader settings const saveSpreaderSettings = async () => { try { if (!spreaderFormData.setting) { toast.error('Please enter a setting value'); return; } // Build payload with correct field names and handle xor validation const payload = { equipmentId: parseInt(selectedEquipmentId), settingValue: spreaderFormData.setting.toString(), // Must be string according to schema rateDescription: spreaderFormData.rateDescription || null, notes: spreaderFormData.notes || null }; // Add either productId OR userProductId, not both (xor validation) if (currentProductForSettings.isShared) { payload.productId = currentProductForSettings.id; } else { payload.userProductId = currentProductForSettings.id; } const response = await fetch('/api/product-spreader-settings', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('authToken')}` }, body: JSON.stringify(payload) }); if (response.ok) { const result = await response.json(); console.log('Spreader setting saved successfully:', result); toast.success('Spreader setting added successfully'); // Store the new setting for calculations if (result.data?.setting) { setSpreaderSettings(prev => ({ ...prev, [`${currentProductForSettings.id}_${selectedEquipmentId}`]: result.data.setting })); } // Close the form setShowSpreaderSettingsForm(false); setCurrentProductForSettings(null); setSpreaderFormData({ setting: '', rateDescription: '', notes: '' }); } else { const errorData = await response.json(); console.error('Failed to save spreader settings:', errorData); throw new Error(errorData.message || 'Failed to save spreader settings'); } } catch (error) { console.error('Failed to save spreader settings:', error); toast.error('Failed to save spreader settings'); } }; // Calculate application amounts const calculateApplicationAmounts = () => { if (!selectedPropertyDetails?.sections || selectedAreas.length === 0) return null; const totalArea = selectedAreas.reduce((total, areaId) => { const area = selectedPropertyDetails.sections.find(s => s.id === areaId); return total + (area?.area || 0); }, 0); const totalAreaSquareMeters = totalArea * 0.092903; // Convert sq ft to sq meters let waterNeeded = 0; // For liquid applications, calculate water needed once for all products if (applicationType === 'liquid' && selectedNozzleId && selectedEquipmentId) { const selectedNozzle = nozzles.find(n => n.id === parseInt(selectedNozzleId)); const selectedEquipment = equipment.find(eq => eq.id === parseInt(selectedEquipmentId)); if (selectedNozzle && selectedEquipment && selectedNozzle.flowRateGpm && selectedEquipment.sprayWidthFeet) { // Convert units const flowRateLPM = selectedNozzle.flowRateGpm * 3.78541; // GPM to L/min const widthMeters = selectedEquipment.sprayWidthFeet * 0.3048; // feet to meters const speedMPM = 107.29; // Assumed speed in meters/minute (4 km/h) // Carrier application rate: Rate (L/m²) = flow (L/min) / (width (m) × speed (m/min)) const carrierRate = flowRateLPM / (widthMeters * speedMPM); // Total water needed = carrier rate × total area const waterNeededLiters = carrierRate * totalAreaSquareMeters; waterNeeded = waterNeededLiters * 0.264172; // Convert liters to gallons } } const calculations = selectedProducts.map(product => { const rateAmount = parseFloat(product.rateAmount) || 0; if (product.productType === 'granular' || product.productType === 'seed') { // Granular calculations - total product needed const totalProductNeeded = (rateAmount * totalArea) / 1000; // Rate is per 1000 sq ft return { productName: product.productName, totalProductNeeded: totalProductNeeded.toFixed(2), unit: product.rateUnit?.replace('/1000sqft', '').replace('/1000 sq ft', '') || 'lbs', waterNeeded: 0 // No water for granular }; } else { // Liquid calculations - total product needed const totalProductNeeded = (rateAmount * totalArea) / 1000; // Rate is per 1000 sq ft return { productName: product.productName, totalProductNeeded: totalProductNeeded.toFixed(2), unit: product.rateUnit?.replace('/1000sqft', '').replace('/1000 sq ft', '') || 'oz', waterNeeded: 0 // Water calculated separately for liquid }; } }); return { totalArea: totalArea.toLocaleString(), calculations, waterNeeded: waterNeeded > 0 ? waterNeeded.toFixed(1) : 0 }; }; // Handle form submission const handleSubmit = async (e) => { e.preventDefault(); // Validation if (!selectedPropertyId) { toast.error('Please select a property'); return; } if (selectedAreas.length === 0) { toast.error('Please select at least one area'); return; } if (!selectedEquipmentId) { toast.error('Please select equipment'); return; } if (selectedProducts.length === 0) { toast.error('Please add at least one product'); return; } if (!plannedDate) { toast.error('Please select a planned date'); return; } if (applicationType === 'liquid' && !selectedNozzleId) { toast.error('Please select a nozzle for liquid applications'); return; } // Validate products have required fields for (const product of selectedProducts) { if (!product.productId && !product.userProductId) { toast.error('Please select a product for all entries'); return; } if (!product.rateAmount) { toast.error('Please enter rate amount for all products'); return; } } setLoading(true); try { const planData = { propertyId: parseInt(selectedPropertyId), selectedAreas, equipmentId: selectedEquipmentId, nozzleId: selectedNozzleId || null, applicationType, plannedDate, notes: applicationType === 'seed' ? `${notes || ''} [Seeding: ${seedMode.replace('_',' ')}]`.trim() : notes }; // Add products in the format expected by Applications.js if (applicationType === 'liquid') { // For liquid: send selectedProducts array with product objects planData.selectedProducts = selectedProducts.map(product => ({ product: products.find(p => (p.uniqueId || p.id) === product.uniqueId), rateAmount: product.rateAmount, rateUnit: product.rateUnit })); } else { // For granular: send single selectedProduct object const firstProduct = selectedProducts[0]; const productDetails = products.find(p => (p.uniqueId || p.id) === firstProduct.uniqueId); planData.selectedProduct = { ...productDetails, customRateAmount: firstProduct.rateAmount, customRateUnit: firstProduct.rateUnit, rateAmount: firstProduct.rateAmount, rateUnit: firstProduct.rateUnit }; } await onSubmit(planData); onClose(); } catch (error) { console.error('Failed to submit plan:', error); toast.error('Failed to save application plan'); } finally { setLoading(false); } }; return (
{/* Header */}

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

{/* Left Column - Form */}
{/* Property Selection */}
{/* Planned Date */}
setPlannedDate(e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2" required />
{/* Application Type & Equipment */}
{/* Application Type */}
{/* Equipment Selection - Required for all */}
{/* Nozzle Selection - Required for liquid applications */} {applicationType === 'liquid' && selectedEquipmentId && (
)}
{/* Seed mode selector */} {applicationType === 'seed' && (
)} {/* Products */}
{applicationType === 'liquid' && ( )}
{selectedProducts.map((product, index) => (
{/* Product Selection */}
{/* Rate Input */}
updateProduct(index, 'rateAmount', e.target.value)} className="w-full border border-gray-300 rounded px-3 py-2" required />
{/* Remove Button */} {applicationType === 'liquid' && selectedProducts.length > 1 && (
)}
))} {selectedProducts.length === 0 && (

{applicationType ? `Select a ${applicationType} product to continue` : 'Select application type first'}

)}
{/* Application Calculations */} {selectedProducts.length > 0 && selectedAreas.length > 0 && (() => { const calculations = calculateApplicationAmounts(); return calculations ? (

Application Calculations

Total Area: {calculations.totalArea} sq ft

{calculations.calculations.map((calc, index) => (

{calc.productName}:

Product needed: {calc.totalProductNeeded} {calc.unit}

))} {applicationType === 'liquid' && calculations.waterNeeded > 0 && (

Water needed: {calculations.waterNeeded} gallons

)}
) : null; })()} {/* Notes */}