Files
turftracker/frontend/src/components/Applications/ApplicationPlanModal.js
Jake Kasper 3fc2bd2ca3 lint stuff
2025-09-04 09:44:27 -05:00

1098 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-6xl mx-4 max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">
{editingPlan ? 'Edit Application Plan' : 'Plan New Application'}
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column - Form */}
<div className="space-y-6">
{/* Property Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<MapPinIcon className="h-4 w-4 inline mr-1" />
Property
</label>
<select
value={selectedPropertyId}
onChange={(e) => handlePropertyChange(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required
>
<option value="">Select a property</option>
{properties.map(property => (
<option key={property.id} value={property.id}>
{property.name} - {property.address}
</option>
))}
</select>
</div>
{/* Planned Date */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Planned Date
</label>
<input
type="date"
value={plannedDate}
onChange={(e) => setPlannedDate(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required
/>
</div>
{/* Application Type & Equipment */}
<div className="border rounded-lg p-4 bg-gray-50">
<div className="grid grid-cols-1 gap-4">
{/* Application Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<BeakerIcon className="h-4 w-4 inline mr-1" />
Application Type
</label>
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
name="applicationType"
value="granular"
checked={applicationType === 'granular'}
onChange={(e) => handleApplicationTypeChange(e.target.value)}
className="mr-2"
/>
Granular (Spreader)
</label>
<label className="flex items-center">
<input
type="radio"
name="applicationType"
value="seed"
checked={applicationType === 'seed'}
onChange={(e) => handleApplicationTypeChange(e.target.value)}
className="mr-2"
/>
Seed (Spreader)
</label>
<label className="flex items-center">
<input
type="radio"
name="applicationType"
value="liquid"
checked={applicationType === 'liquid'}
onChange={(e) => handleApplicationTypeChange(e.target.value)}
className="mr-2"
/>
Liquid (Sprayer)
</label>
</div>
</div>
{/* Equipment Selection - Required for all */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<WrenchScrewdriverIcon className="h-4 w-4 inline mr-1" />
Equipment *
</label>
<select
value={selectedEquipmentId}
onChange={(e) => setSelectedEquipmentId(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required
>
<option value="">Select equipment</option>
{equipment
.filter(eq => {
if (!applicationType) return true;
// Filter by application type - granular needs Spreader, liquid needs Sprayer
if (applicationType === 'granular' || applicationType === 'seed') {
return eq.categoryName === 'Spreader';
} else if (applicationType === 'liquid') {
return eq.categoryName === 'Sprayer';
}
return true;
})
.map(eq => (
<option key={eq.id} value={eq.id}>
{eq.customName} - {eq.typeName}
</option>
))}
</select>
</div>
{/* Nozzle Selection - Required for liquid applications */}
{applicationType === 'liquid' && selectedEquipmentId && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nozzle Selection *
<span className="text-xs text-gray-500 ml-1">(Required for spray calculations)</span>
</label>
<select
value={selectedNozzleId}
onChange={(e) => setSelectedNozzleId(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required={applicationType === 'liquid'}
>
<option value="">Select nozzle</option>
{nozzles.map(nozzle => (
<option key={nozzle.id} value={nozzle.id}>
{nozzle.customName} - {nozzle.manufacturer}
{nozzle.flowRateGpm && ` (${nozzle.flowRateGpm} GPM)`}
{nozzle.orificeSize && ` - ${nozzle.orificeSize}`}
</option>
))}
</select>
</div>
)}
</div>
</div>
{/* Seed mode selector */}
{applicationType === 'seed' && (
<div className="mb-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Seeding Scenario</label>
<div className="flex items-center gap-4 text-sm">
<label className="inline-flex items-center gap-2">
<input type="radio" name="seed-mode" value="overseed" checked={seedMode==='overseed'} onChange={()=> setSeedMode('overseed')} />
Overseed
</label>
<label className="inline-flex items-center gap-2">
<input type="radio" name="seed-mode" value="new_seed" checked={seedMode==='new_seed'} onChange={()=> setSeedMode('new_seed')} />
New lawn
</label>
</div>
</div>
)}
{/* Products */}
<div>
<div className="flex justify-between items-center mb-2">
<label className="block text-sm font-medium text-gray-700">
Products to Apply
</label>
{applicationType === 'liquid' && (
<button
type="button"
onClick={addProduct}
className="text-blue-600 hover:text-blue-800 text-sm"
>
+ Add Product (Tank Mix)
</button>
)}
</div>
{selectedProducts.map((product, index) => (
<div key={index} className="border rounded p-3 mb-3">
{/* Product Selection */}
<div className="grid grid-cols-1 gap-3">
<select
value={product.uniqueId || ''}
onChange={(e) => {
const selectedProduct = products.find(p => (p.uniqueId || p.id) === e.target.value);
if (selectedProduct) {
updateProduct(index, 'uniqueId', e.target.value);
updateProduct(index, 'productId', selectedProduct.isShared ? selectedProduct.id : null);
updateProduct(index, 'userProductId', !selectedProduct.isShared ? selectedProduct.id : null);
updateProduct(index, 'isUserProduct', !selectedProduct.isShared);
// Set product name and brand based on whether it's shared or custom
const productName = selectedProduct.isShared ?
selectedProduct.name :
(selectedProduct.customName || selectedProduct.baseProductName || selectedProduct.name);
const productBrand = selectedProduct.isShared ?
selectedProduct.brand :
(selectedProduct.customBrand || selectedProduct.brand || 'Unknown Brand');
updateProduct(index, 'productName', productName);
updateProduct(index, 'productBrand', productBrand);
updateProduct(index, 'productType', selectedProduct.productType);
// Pre-populate application rate if available
console.log('Selected product for rate population:', selectedProduct);
let rateSet = false;
// For shared products: check rates array
if (selectedProduct.isShared && selectedProduct.rates && selectedProduct.rates.length > 0) {
let defaultRate = selectedProduct.rates[0];
if (applicationType === 'seed') {
const modeKey = seedMode === 'new_seed' ? 'new' : 'over';
const match = selectedProduct.rates.find(r => {
const t = (r.applicationType || '').toLowerCase();
return t.includes('seed') && t.includes(modeKey);
});
if (match) defaultRate = match;
}
updateProduct(index, 'rateAmount', defaultRate.rateAmount || defaultRate.amount);
updateProduct(index, 'rateUnit', defaultRate.rateUnit || defaultRate.unit);
rateSet = true;
}
// For custom products: check customRateAmount and customRateUnit
else if (!selectedProduct.isShared && selectedProduct.customRateAmount) {
updateProduct(index, 'rateAmount', selectedProduct.customRateAmount);
updateProduct(index, 'rateUnit', selectedProduct.customRateUnit || (selectedProduct.productType === 'granular' ? 'lb/1000sqft' : 'oz/1000sqft'));
rateSet = true;
}
// Set default rate unit if no rate was found
if (!rateSet) {
if (selectedProduct.productType === 'granular' || selectedProduct.productType === 'seed') {
updateProduct(index, 'rateUnit', 'lb/1000sqft');
} else if (selectedProduct.productType === 'liquid') {
updateProduct(index, 'rateUnit', 'oz/1000sqft');
}
}
// For granular or seed products, check if spreader settings exist
if ((selectedProduct.productType === 'granular' || selectedProduct.productType === 'seed') && selectedEquipmentId) {
checkSpreaderSettings(selectedProduct, selectedEquipmentId);
}
}
}}
className="border border-gray-300 rounded px-3 py-2"
required
>
<option value="">Select product</option>
{products
.filter(prod => !applicationType || prod.productType === applicationType)
.map(prod => (
<option key={prod.uniqueId || prod.id} value={prod.uniqueId || prod.id}>
{/* For shared products: use name and brand */}
{/* For custom products: use customName or name, and customBrand or brand */}
{prod.isShared ?
`${prod.name} - ${prod.brand}` :
`${prod.customName || prod.baseProductName || prod.name} - ${prod.customBrand || prod.brand || 'Unknown Brand'}`
}
{prod.isShared === false && ' (Custom)'}
</option>
))}
</select>
{/* Rate Input */}
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs text-gray-600 mb-1">Application Rate</label>
<input
type="number"
step="0.01"
placeholder="Rate"
value={product.rateAmount || ''}
onChange={(e) => updateProduct(index, 'rateAmount', e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1">Rate Unit</label>
<select
value={product.rateUnit || ((applicationType === 'granular' || applicationType === 'seed') ? 'lb/1000sqft' : 'oz/1000sqft')}
onChange={(e) => updateProduct(index, 'rateUnit', e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
>
{(applicationType === 'granular' || applicationType === 'seed') ? (
<>
<option value="lb/1000sqft">lb/1000sqft</option>
<option value="oz/1000sqft">oz/1000sqft</option>
</>
) : (
<>
<option value="oz/1000sqft">oz/1000sqft</option>
<option value="fl oz/1000sqft">fl oz/1000sqft</option>
<option value="lb/1000sqft">lb/1000sqft</option>
</>
)}
</select>
</div>
</div>
{/* Remove Button */}
{applicationType === 'liquid' && selectedProducts.length > 1 && (
<div className="flex justify-end">
<button
type="button"
onClick={() => removeProduct(index)}
className="text-red-600 hover:text-red-800 text-sm"
>
Remove Product
</button>
</div>
)}
</div>
</div>
))}
{selectedProducts.length === 0 && (
<div className="text-center py-4 border-2 border-dashed border-gray-300 rounded">
<p className="text-gray-500 text-sm">
{applicationType ? `Select a ${applicationType} product to continue` : 'Select application type first'}
</p>
</div>
)}
</div>
{/* Application Calculations */}
{selectedProducts.length > 0 && selectedAreas.length > 0 && (() => {
const calculations = calculateApplicationAmounts();
return calculations ? (
<div className="border rounded-lg p-4 bg-blue-50">
<h3 className="font-medium text-blue-900 mb-2">Application Calculations</h3>
<div className="text-sm text-blue-800">
<p className="mb-2">Total Area: {calculations.totalArea} sq ft</p>
{calculations.calculations.map((calc, index) => (
<div key={index} className="mb-2">
<p className="font-medium">{calc.productName}:</p>
<p className="ml-2">Product needed: {calc.totalProductNeeded} {calc.unit}</p>
</div>
))}
{applicationType === 'liquid' && calculations.waterNeeded > 0 && (
<div className="mt-2 pt-2 border-t border-blue-200">
<p className="font-medium">Water needed: {calculations.waterNeeded} gallons</p>
</div>
)}
</div>
</div>
) : null;
})()}
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Notes (Optional)
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
className="w-full border border-gray-300 rounded px-3 py-2"
placeholder="Add any notes or special instructions..."
/>
</div>
</div>
{/* Right Column - Areas & Map */}
<div>
{/* Area Selection with Checkboxes */}
{selectedPropertyDetails?.sections?.length > 0 && (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Areas to Treat
</label>
<div className="max-h-32 overflow-y-auto border rounded p-2 bg-gray-50">
{selectedPropertyDetails.sections.map(section => (
<label key={section.id} className="flex items-center py-1 cursor-pointer hover:bg-gray-100 rounded px-2">
<input
type="checkbox"
checked={selectedAreas.includes(section.id)}
onChange={() => handleAreaClick(section)}
className="mr-2"
/>
<span className="text-sm">
{section.name} ({Math.round(section.area || 0).toLocaleString()} sq ft)
</span>
</label>
))}
</div>
{selectedAreas.length > 0 && (
<div className="mt-2 p-2 bg-blue-50 rounded">
<p className="text-sm font-medium text-blue-800">
Total Selected: {selectedAreas.reduce((total, areaId) => {
const area = selectedPropertyDetails.sections?.find(s => s.id === areaId);
return total + (area?.area || 0);
}, 0).toLocaleString()} sq ft
</p>
</div>
)}
</div>
)}
{/* Map Display */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Property Map
</label>
<div className="h-64 border rounded-lg overflow-hidden">
{selectedPropertyDetails ? (
<PropertyMap
center={mapCenter}
zoom={16}
property={selectedPropertyDetails}
sections={selectedPropertyDetails.sections || []}
selectedSections={selectedAreas}
mode="view"
editable={false}
className="h-full w-full"
/>
) : (
<div className="h-full w-full flex items-center justify-center bg-gray-100">
<p className="text-gray-500">Select a property to view areas</p>
</div>
)}
</div>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end gap-3 mt-6 pt-6 border-t">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
disabled={loading}
>
{loading ? 'Saving...' : (editingPlan ? 'Update Plan' : 'Create Plan')}
</button>
</div>
</form>
</div>
{/* Spreader Settings Form Modal */}
{showSpreaderSettingsForm && currentProductForSettings && (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center" style={{zIndex: 9999}}>
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<h3 className="text-lg font-semibold mb-4">Add New Setting</h3>
{/* Equipment Dropdown */}
<div className="mb-4">
<select
disabled
value={selectedEquipmentId}
className="w-full border border-gray-300 rounded px-3 py-2 bg-gray-100"
>
{equipment
.filter(eq => eq.id === parseInt(selectedEquipmentId))
.map(eq => (
<option key={eq.id} value={eq.id}>
{eq.customName} ({eq.categoryName?.toLowerCase()})
</option>
))}
</select>
</div>
{/* Setting Input */}
<div className="mb-4">
<input
type="text"
value={spreaderFormData.setting}
onChange={(e) => setSpreaderFormData(prev => ({
...prev,
setting: e.target.value
}))}
className="w-full border border-green-400 rounded px-3 py-2 focus:outline-none focus:border-green-500"
placeholder="Setting (e.g., #14, 2.5)"
/>
</div>
{/* Rate Description */}
<div className="mb-4">
<input
type="text"
value={spreaderFormData.rateDescription}
onChange={(e) => setSpreaderFormData(prev => ({
...prev,
rateDescription: e.target.value
}))}
className="w-full border border-gray-300 rounded px-3 py-2"
placeholder="Rate description (optional)"
/>
</div>
{/* Notes */}
<div className="mb-6">
<textarea
rows={3}
value={spreaderFormData.notes}
onChange={(e) => setSpreaderFormData(prev => ({
...prev,
notes: e.target.value
}))}
className="w-full border border-gray-300 rounded px-3 py-2"
placeholder="Notes (optional)"
/>
</div>
<div className="flex justify-end gap-3">
<button
type="button"
onClick={() => {
setShowSpreaderSettingsForm(false);
setCurrentProductForSettings(null);
setSpreaderFormData({
setting: '',
rateDescription: '',
notes: ''
});
}}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Cancel
</button>
<button
type="button"
onClick={saveSpreaderSettings}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Add Setting
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default ApplicationPlanModal;