maybe fixed?
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
// This is a placeholder - the ApplicationPlanModal component was in the main file
|
||||
// We need to extract it to this separate file for better organization
|
||||
// For now, the modal functionality is temporarily unavailable until we recreate it
|
||||
|
||||
const ApplicationPlanModal = () => {
|
||||
return <div>ApplicationPlanModal - To be implemented</div>;
|
||||
};
|
||||
|
||||
export default ApplicationPlanModal;
|
||||
@@ -581,839 +581,4 @@ const Applications = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Application Planning Modal Component
|
||||
const ApplicationPlanModal = ({
|
||||
onClose,
|
||||
onSubmit,
|
||||
properties,
|
||||
products,
|
||||
equipment,
|
||||
nozzles,
|
||||
selectedPropertyDetails,
|
||||
onPropertySelect,
|
||||
editingPlan
|
||||
}) => {
|
||||
const [loadingProperty, setLoadingProperty] = useState(false);
|
||||
const [showSpreaderSettingPrompt, setShowSpreaderSettingPrompt] = useState(false);
|
||||
const [missingSpreaderSetting, setMissingSpreaderSetting] = useState({
|
||||
productId: null,
|
||||
userProductId: null,
|
||||
equipmentId: null,
|
||||
productName: '',
|
||||
equipmentName: ''
|
||||
});
|
||||
const [newSpreaderSetting, setNewSpreaderSetting] = useState({
|
||||
settingValue: '',
|
||||
rateDescription: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
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: ''
|
||||
});
|
||||
|
||||
// Reset form when modal opens fresh (not editing)
|
||||
useEffect(() => {
|
||||
if (!editingPlan) {
|
||||
setPlanData({
|
||||
propertyId: '',
|
||||
selectedAreas: [],
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: [],
|
||||
applicationType: '',
|
||||
equipmentId: '',
|
||||
nozzleId: '',
|
||||
plannedDate: '',
|
||||
notes: ''
|
||||
});
|
||||
}
|
||||
}, [editingPlan]);
|
||||
|
||||
// Check spreader settings when both product and equipment are selected for granular applications
|
||||
useEffect(() => {
|
||||
console.log('Spreader setting useEffect triggered:', {
|
||||
applicationType: planData.applicationType,
|
||||
hasSelectedProduct: !!planData.selectedProduct,
|
||||
hasEquipmentId: !!planData.equipmentId,
|
||||
productName: planData.selectedProduct?.customName || planData.selectedProduct?.name,
|
||||
equipmentId: planData.equipmentId,
|
||||
isEditing: !!editingPlan
|
||||
});
|
||||
|
||||
if (planData.applicationType === 'granular' &&
|
||||
planData.selectedProduct &&
|
||||
planData.equipmentId) {
|
||||
console.log('Triggering spreader setting check from useEffect');
|
||||
checkSpreaderSetting(planData.selectedProduct, planData.equipmentId);
|
||||
}
|
||||
}, [planData.applicationType, planData.selectedProduct, planData.equipmentId]);
|
||||
|
||||
// Initialize form with editing data
|
||||
useEffect(() => {
|
||||
if (editingPlan && products.length > 0) {
|
||||
const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id;
|
||||
|
||||
// Determine application type from the first product
|
||||
const firstProduct = editingPlan.products?.[0];
|
||||
let applicationType = 'liquid'; // Default to liquid
|
||||
|
||||
if (firstProduct) {
|
||||
const productType = firstProduct.productType || firstProduct.customProductType;
|
||||
// Use flexible matching like elsewhere in the codebase
|
||||
const isGranular = productType && (
|
||||
productType.toLowerCase().includes('granular') ||
|
||||
productType.toLowerCase().includes('granule')
|
||||
);
|
||||
|
||||
// Also check equipment type as fallback for granular detection
|
||||
const equipmentType = editingPlan.equipment?.categoryName?.toLowerCase() || '';
|
||||
const isGranularByEquipment = equipmentType.includes('spreader') || equipmentType.includes('granular');
|
||||
|
||||
// If we can't determine from product type, use equipment type
|
||||
applicationType = (isGranular || (!productType && isGranularByEquipment)) ? 'granular' : 'liquid';
|
||||
|
||||
console.log('Edit plan - application type detection:', {
|
||||
productType: productType,
|
||||
isGranular: isGranular,
|
||||
equipmentType: equipmentType,
|
||||
isGranularByEquipment: isGranularByEquipment,
|
||||
applicationType: applicationType,
|
||||
productName: firstProduct.productName,
|
||||
fullProduct: firstProduct
|
||||
});
|
||||
}
|
||||
|
||||
// Handle different application types
|
||||
if (applicationType === 'granular') {
|
||||
// Granular - single product
|
||||
let selectedProduct = null;
|
||||
if (firstProduct) {
|
||||
if (firstProduct.productId) {
|
||||
selectedProduct = products.find(p => p.uniqueId === `shared_${firstProduct.productId}`);
|
||||
} else if (firstProduct.userProductId) {
|
||||
selectedProduct = products.find(p => p.uniqueId === `user_${firstProduct.userProductId}`);
|
||||
}
|
||||
}
|
||||
|
||||
setPlanData({
|
||||
propertyId: propertyId?.toString() || '',
|
||||
selectedAreas: editingPlan.sections?.map(s => s.id) || [], // Handle multiple areas
|
||||
productId: selectedProduct?.uniqueId || '',
|
||||
selectedProduct: selectedProduct,
|
||||
selectedProducts: [],
|
||||
applicationType: 'granular',
|
||||
equipmentId: editingPlan.equipment?.id?.toString() || '',
|
||||
nozzleId: editingPlan.nozzle?.id?.toString() || '',
|
||||
plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '',
|
||||
notes: editingPlan.notes || ''
|
||||
});
|
||||
} else {
|
||||
// Liquid - multiple products (tank mix)
|
||||
const selectedProducts = editingPlan.products?.map(product => {
|
||||
let foundProduct = null;
|
||||
if (product.productId) {
|
||||
foundProduct = products.find(p => p.uniqueId === `shared_${product.productId}`);
|
||||
} else if (product.userProductId) {
|
||||
foundProduct = products.find(p => p.uniqueId === `user_${product.userProductId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
product: foundProduct,
|
||||
rateAmount: product.rateAmount || 1,
|
||||
rateUnit: product.rateUnit || 'oz/1000 sq ft'
|
||||
};
|
||||
}).filter(item => item.product) || [];
|
||||
|
||||
setPlanData({
|
||||
propertyId: propertyId?.toString() || '',
|
||||
selectedAreas: editingPlan.sections?.map(s => s.id) || [], // Handle multiple areas
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: selectedProducts,
|
||||
applicationType: 'liquid',
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// Check for spreader settings when granular product + equipment selected
|
||||
const checkSpreaderSetting = async (product, equipmentId) => {
|
||||
if (!product || !equipmentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if product is granular (more flexible matching like the product filter)
|
||||
const productType = product.productType || product.customProductType;
|
||||
const isGranular = productType && (
|
||||
productType.toLowerCase().includes('granular') ||
|
||||
productType.toLowerCase().includes('granule')
|
||||
);
|
||||
|
||||
if (!isGranular) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedEquipment = equipment.find(eq => eq.id === parseInt(equipmentId));
|
||||
if (!selectedEquipment) {
|
||||
console.log('Spreader setting check skipped - equipment not found:', {
|
||||
equipmentId: equipmentId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if equipment is spreader-related (more flexible matching)
|
||||
const categoryName = selectedEquipment.categoryName?.toLowerCase() || '';
|
||||
const isSpreaderEquipment = categoryName.includes('spreader') || categoryName.includes('granular');
|
||||
|
||||
if (!isSpreaderEquipment) {
|
||||
console.log('Spreader setting check skipped - equipment not a spreader:', {
|
||||
selectedEquipment: selectedEquipment?.customName,
|
||||
category: selectedEquipment?.categoryName
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Checking spreader setting for:', {
|
||||
product: product.customName || product.name,
|
||||
equipment: selectedEquipment.customName,
|
||||
productType: productType
|
||||
});
|
||||
|
||||
try {
|
||||
// Check if spreader setting exists
|
||||
const endpoint = product.isShared
|
||||
? `/api/product-spreader-settings/product/${product.productId || product.id}`
|
||||
: `/api/product-spreader-settings/user-product/${product.id}`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const settings = data.data?.settings || [];
|
||||
|
||||
// Check if there's a setting for this specific equipment
|
||||
const existingSetting = settings.find(setting =>
|
||||
setting.equipmentId === selectedEquipment.id
|
||||
);
|
||||
|
||||
if (!existingSetting) {
|
||||
// No setting found - prompt user to create one
|
||||
setMissingSpreaderSetting({
|
||||
productId: product.isShared ? (product.productId || product.id) : null,
|
||||
userProductId: product.isShared ? null : product.id,
|
||||
equipmentId: selectedEquipment.id,
|
||||
productName: product.customName || product.name,
|
||||
equipmentName: selectedEquipment.customName
|
||||
});
|
||||
setShowSpreaderSettingPrompt(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check spreader settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Save new spreader setting
|
||||
const saveSpreaderSetting = async () => {
|
||||
try {
|
||||
const settingData = {
|
||||
...(missingSpreaderSetting.productId && { productId: missingSpreaderSetting.productId }),
|
||||
...(missingSpreaderSetting.userProductId && { userProductId: missingSpreaderSetting.userProductId }),
|
||||
equipmentId: missingSpreaderSetting.equipmentId,
|
||||
settingValue: newSpreaderSetting.settingValue,
|
||||
rateDescription: newSpreaderSetting.rateDescription || null,
|
||||
notes: newSpreaderSetting.notes || null
|
||||
};
|
||||
|
||||
const response = await fetch('/api/product-spreader-settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
||||
},
|
||||
body: JSON.stringify(settingData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Spreader setting saved successfully');
|
||||
setShowSpreaderSettingPrompt(false);
|
||||
setNewSpreaderSetting({ settingValue: '', rateDescription: '', notes: '' });
|
||||
} else {
|
||||
toast.error('Failed to save spreader setting');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save spreader setting:', error);
|
||||
toast.error('Failed to save spreader setting');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 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 (
|
||||
<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-4xl max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{editingPlan ? 'Edit Application Plan' : 'Plan Application'}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Property Selection */}
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<MapPinIcon className="h-5 w-5" />
|
||||
Property *
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={planData.propertyId}
|
||||
onChange={(e) => handlePropertyChange(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Select a property...</option>
|
||||
{properties.map((property) => (
|
||||
<option key={property.id} value={property.id}>
|
||||
{property.name} - {property.address}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Area Selection with Map */}
|
||||
{loadingProperty && (
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<LoadingSpinner size="sm" />
|
||||
<span>Loading property details...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPropertyDetails && (
|
||||
<div>
|
||||
<label className="label">Application Areas *</label>
|
||||
|
||||
{/* Property Map */}
|
||||
{selectedPropertyDetails.latitude && selectedPropertyDetails.longitude && (
|
||||
<div className="mb-4">
|
||||
<div className="relative">
|
||||
<PropertyMap
|
||||
center={[selectedPropertyDetails.latitude, selectedPropertyDetails.longitude]}
|
||||
zoom={16}
|
||||
property={selectedPropertyDetails}
|
||||
sections={selectedPropertyDetails.sections || []}
|
||||
editable={false}
|
||||
className="h-64 w-full"
|
||||
selectedSections={planData.selectedAreas}
|
||||
onSectionClick={(section) => handleAreaToggle(section.id)}
|
||||
/>
|
||||
<div className="absolute top-2 left-2 bg-white rounded-lg shadow-lg p-2 text-xs">
|
||||
<p className="font-medium text-gray-700">Click sections to select</p>
|
||||
{planData.selectedAreas.length > 0 && (
|
||||
<p className="text-blue-600">{planData.selectedAreas.length} selected</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPropertyDetails.sections && selectedPropertyDetails.sections.length > 0 ? (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{selectedPropertyDetails.sections.map((section) => (
|
||||
<label key={section.id} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={planData.selectedAreas.includes(section.id)}
|
||||
onChange={() => handleAreaToggle(section.id)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-2 text-sm">
|
||||
{section.name} ({section.area ? `${Math.round(section.area)} sq ft` : 'No size'})
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{planData.selectedAreas.length > 0 && (
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
Total area: {selectedPropertyDetails.sections
|
||||
.filter(s => planData.selectedAreas.includes(s.id))
|
||||
.reduce((total, s) => total + (s.area || 0), 0).toFixed(0)} sq ft
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-yellow-800 text-sm">
|
||||
This property has no lawn sections defined. Please add lawn sections to the property first.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Application Type Selection */}
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
Application Type *
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={planData.applicationType}
|
||||
onChange={(e) => {
|
||||
setPlanData({
|
||||
...planData,
|
||||
applicationType: e.target.value,
|
||||
productId: '',
|
||||
selectedProduct: null,
|
||||
selectedProducts: []
|
||||
});
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select application type...</option>
|
||||
<option value="liquid">Liquid (Tank Mix)</option>
|
||||
<option value="granular">Granular</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Product Selection - Single for Granular */}
|
||||
{planData.applicationType === 'granular' && (
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
Product *
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={planData.productId}
|
||||
onChange={(e) => {
|
||||
const selectedProduct = products.find(p => p.uniqueId === e.target.value);
|
||||
setPlanData({
|
||||
...planData,
|
||||
productId: e.target.value,
|
||||
selectedProduct: selectedProduct
|
||||
});
|
||||
// Check for spreader settings when granular product and spreader are both selected
|
||||
if (selectedProduct && planData.equipmentId) {
|
||||
checkSpreaderSetting(selectedProduct, planData.equipmentId);
|
||||
}
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select a product...</option>
|
||||
{products.filter(product => {
|
||||
const productType = product.productType || product.customProductType;
|
||||
return productType && (productType.toLowerCase().includes('granular') || productType.toLowerCase().includes('granule'));
|
||||
}).map((product) => {
|
||||
const displayName = product.customName || product.name;
|
||||
const brand = product.brand || product.customBrand;
|
||||
const rateInfo = product.customRateAmount && product.customRateUnit
|
||||
? ` (${product.customRateAmount} ${product.customRateUnit})`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<option key={product.uniqueId} value={product.uniqueId}>
|
||||
{displayName}{brand ? ` - ${brand}` : ''}{rateInfo}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Selection - Multiple for Liquid Tank Mix */}
|
||||
{planData.applicationType === 'liquid' && (
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
Tank Mix Products *
|
||||
</label>
|
||||
|
||||
{/* Selected Products List */}
|
||||
{planData.selectedProducts.length > 0 && (
|
||||
<div className="space-y-2 mb-3">
|
||||
{planData.selectedProducts.map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2 p-2 bg-blue-50 border border-blue-200 rounded">
|
||||
<div className="flex-1">
|
||||
<span className="font-medium">{item.product.customName || item.product.name}</span>
|
||||
{item.product.brand || item.product.customBrand ? (
|
||||
<span className="text-gray-600"> - {item.product.brand || item.product.customBrand}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={item.rateAmount || ''}
|
||||
onChange={(e) => {
|
||||
const newProducts = [...planData.selectedProducts];
|
||||
newProducts[index] = {
|
||||
...item,
|
||||
rateAmount: e.target.value === '' ? '' : parseFloat(e.target.value)
|
||||
};
|
||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
||||
}}
|
||||
className="w-24 px-2 py-1 text-sm border rounded"
|
||||
placeholder="Rate"
|
||||
/>
|
||||
<select
|
||||
value={item.rateUnit || 'oz/1000 sq ft'}
|
||||
onChange={(e) => {
|
||||
const newProducts = [...planData.selectedProducts];
|
||||
newProducts[index] = {
|
||||
...item,
|
||||
rateUnit: e.target.value
|
||||
};
|
||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
||||
}}
|
||||
className="text-sm border rounded px-1 py-1"
|
||||
>
|
||||
<option value="oz/1000 sq ft">oz/1000 sq ft</option>
|
||||
<option value="oz/acre">oz/acre</option>
|
||||
<option value="fl oz/1000 sq ft">fl oz/1000 sq ft</option>
|
||||
<option value="ml/1000 sq ft">ml/1000 sq ft</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newProducts = planData.selectedProducts.filter((_, i) => i !== index);
|
||||
setPlanData({ ...planData, selectedProducts: newProducts });
|
||||
}}
|
||||
className="text-red-600 hover:text-red-800 p-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Product Dropdown */}
|
||||
<select
|
||||
className="input"
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
const selectedProduct = products.find(p => p.uniqueId === e.target.value);
|
||||
if (selectedProduct && !planData.selectedProducts.some(p => p.product.uniqueId === selectedProduct.uniqueId)) {
|
||||
const newProduct = {
|
||||
product: selectedProduct,
|
||||
rateAmount: selectedProduct.customRateAmount || 1,
|
||||
rateUnit: selectedProduct.customRateUnit || 'oz/1000 sq ft'
|
||||
};
|
||||
setPlanData({
|
||||
...planData,
|
||||
selectedProducts: [...planData.selectedProducts, newProduct]
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Add a product to tank mix...</option>
|
||||
{products.filter(product => {
|
||||
const productType = product.productType || product.customProductType;
|
||||
const isLiquid = productType && (productType.toLowerCase().includes('liquid') || productType.toLowerCase().includes('concentrate'));
|
||||
const notAlreadySelected = !planData.selectedProducts.some(p => p.product.uniqueId === product.uniqueId);
|
||||
return isLiquid && notAlreadySelected;
|
||||
}).map((product) => {
|
||||
const displayName = product.customName || product.name;
|
||||
const brand = product.brand || product.customBrand;
|
||||
const rateInfo = product.customRateAmount && product.customRateUnit
|
||||
? ` (${product.customRateAmount} ${product.customRateUnit})`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<option key={product.uniqueId} value={product.uniqueId}>
|
||||
{displayName}{brand ? ` - ${brand}` : ''}{rateInfo}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
|
||||
{planData.selectedProducts.length === 0 && (
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Select liquid products to mix in the tank. You can add herbicides, surfactants, and other liquid products.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Equipment Selection */}
|
||||
{planData.applicationType && (
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<WrenchScrewdriverIcon className="h-5 w-5" />
|
||||
Equipment * ({planData.applicationType})
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={planData.equipmentId}
|
||||
onChange={(e) => {
|
||||
const newEquipmentId = e.target.value;
|
||||
setPlanData({ ...planData, equipmentId: newEquipmentId });
|
||||
// Check for spreader settings when granular product and spreader are both selected
|
||||
if (planData.applicationType === 'granular' && planData.selectedProduct && newEquipmentId) {
|
||||
checkSpreaderSetting(planData.selectedProduct, newEquipmentId);
|
||||
}
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">Select equipment...</option>
|
||||
{availableEquipment.map((eq) => (
|
||||
<option key={eq.id} value={eq.id}>
|
||||
{eq.customName || eq.typeName}
|
||||
{eq.manufacturer && ` - ${eq.manufacturer}`}
|
||||
{eq.tankSizeGallons && ` (${eq.tankSizeGallons} gal)`}
|
||||
{eq.capacityLbs && ` (${eq.capacityLbs} lbs)`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{availableEquipment.length === 0 && (
|
||||
<p className="text-sm text-orange-600 mt-1">
|
||||
No {planData.applicationType === 'liquid' ? 'sprayers' : 'spreaders'} found.
|
||||
Please add equipment first.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spreader Recommendation for Granular Applications */}
|
||||
|
||||
{/* Nozzle Selection for Liquid Applications */}
|
||||
{planData.applicationType === 'liquid' && (
|
||||
<div>
|
||||
<label className="label flex items-center gap-2">
|
||||
<BeakerIcon className="h-5 w-5" />
|
||||
Nozzle Selection
|
||||
</label>
|
||||
<select
|
||||
className="input"
|
||||
value={planData.nozzleId}
|
||||
onChange={(e) => setPlanData({ ...planData, nozzleId: e.target.value })}
|
||||
>
|
||||
<option value="">Select nozzle (optional)...</option>
|
||||
{nozzles.map((nozzle) => (
|
||||
<option key={nozzle.id} value={nozzle.id}>
|
||||
{nozzle.customName || nozzle.typeName}
|
||||
{nozzle.orificeSize && ` - ${nozzle.orificeSize}`}
|
||||
{nozzle.flowRateGpm && ` (${nozzle.flowRateGpm} GPM)`}
|
||||
{nozzle.sprayAngle && ` ${nozzle.sprayAngle}°`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{nozzles.length === 0 && (
|
||||
<p className="text-sm text-orange-600 mt-1">
|
||||
No nozzles found. Go to Equipment → Add Equipment → Select "Nozzle" category to add nozzles first.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Planned Date */}
|
||||
<div>
|
||||
<label className="label">Planned Date</label>
|
||||
<input
|
||||
type="date"
|
||||
className="input"
|
||||
value={planData.plannedDate}
|
||||
onChange={(e) => setPlanData({ ...planData, plannedDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div>
|
||||
<label className="label">Notes</label>
|
||||
<textarea
|
||||
className="input"
|
||||
rows="3"
|
||||
value={planData.notes}
|
||||
onChange={(e) => setPlanData({ ...planData, notes: e.target.value })}
|
||||
placeholder="Application notes, weather conditions, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="submit" className="btn-primary flex-1">
|
||||
{editingPlan ? 'Update Application Plan' : 'Create Application Plan'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Spreader Setting Prompt Modal */}
|
||||
{showSpreaderSettingPrompt && (
|
||||
<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-md">
|
||||
<h3 className="text-lg font-semibold mb-4">Spreader Setting Required</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
No spreader setting found for <strong>{missingSpreaderSetting.productName}</strong> with <strong>{missingSpreaderSetting.equipmentName}</strong>.
|
||||
<br />Please enter the spreader setting to continue.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="label">Spreader Setting *</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
value={newSpreaderSetting.settingValue}
|
||||
onChange={(e) => setNewSpreaderSetting({
|
||||
...newSpreaderSetting,
|
||||
settingValue: e.target.value
|
||||
})}
|
||||
placeholder="e.g., 3.5, B, 4"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Rate Description (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
value={newSpreaderSetting.rateDescription}
|
||||
onChange={(e) => setNewSpreaderSetting({
|
||||
...newSpreaderSetting,
|
||||
rateDescription: e.target.value
|
||||
})}
|
||||
placeholder="e.g., 2 lbs per 1000 sq ft"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="label">Notes (Optional)</label>
|
||||
<textarea
|
||||
className="input"
|
||||
rows="2"
|
||||
value={newSpreaderSetting.notes}
|
||||
onChange={(e) => setNewSpreaderSetting({
|
||||
...newSpreaderSetting,
|
||||
notes: e.target.value
|
||||
})}
|
||||
placeholder="Additional notes or instructions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<button
|
||||
onClick={saveSpreaderSetting}
|
||||
className="btn-primary flex-1"
|
||||
disabled={!newSpreaderSetting.settingValue}
|
||||
>
|
||||
Save Setting
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowSpreaderSettingPrompt(false);
|
||||
setNewSpreaderSetting({ settingValue: '', rateDescription: '', notes: '' });
|
||||
}}
|
||||
className="btn-secondary flex-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
};
|
||||
|
||||
export default Applications;
|
||||
|
||||
Reference in New Issue
Block a user