Files
turftracker/frontend/src/components/Applications/ApplicationPlanModal.js
Jake Kasper e22cc10310 asdfas
2025-08-28 08:04:12 -05:00

990 lines
39 KiB
JavaScript

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('');
const [plannedDate, setPlannedDate] = useState('');
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) {
setSelectedPropertyId(editingPlan.propertyId || '');
setSelectedAreas(editingPlan.selectedAreas || []);
setSelectedEquipmentId(editingPlan.equipmentId || '');
setSelectedNozzleId(editingPlan.nozzleId || '');
setSelectedProducts(editingPlan.products || []);
setPlannedDate(editingPlan.plannedDate || '');
setNotes(editingPlan.notes || '');
}
}, [editingPlan]);
// 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' ? 'lb/1000sqft' : 'oz/1000sqft',
isUserProduct: false
}]);
};
// Handle application type change
const handleApplicationTypeChange = (type) => {
setApplicationType(type);
// Clear products when switching type
setSelectedProducts([]);
// Add initial product
setTimeout(() => {
setSelectedProducts([{
uniqueId: '',
productId: null,
userProductId: null,
productName: '',
productBrand: '',
productType: type,
rateAmount: '',
rateUnit: type === 'granular' ? '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 calculations = selectedProducts.map(product => {
const rateAmount = parseFloat(product.rateAmount) || 0;
const areaIn1000s = totalArea / 1000;
if (product.productType === 'granular') {
// Granular calculations
const totalProductNeeded = rateAmount * areaIn1000s;
return {
productName: product.productName,
totalProductNeeded: totalProductNeeded.toFixed(2),
unit: product.rateUnit,
waterNeeded: 0 // No water for granular
};
} else {
// Liquid calculations
const totalProductNeeded = rateAmount * areaIn1000s;
// Calculate water needed based on nozzle and equipment settings
let waterNeeded = 0;
if (selectedNozzleId) {
const selectedNozzle = nozzles.find(n => n.id === parseInt(selectedNozzleId));
if (selectedNozzle && selectedNozzle.flowRateGpm) {
// Basic calculation - this would need equipment speed settings for accuracy
waterNeeded = (selectedNozzle.flowRateGpm * 60 * areaIn1000s) / 1000; // Rough estimate
}
}
return {
productName: product.productName,
totalProductNeeded: totalProductNeeded.toFixed(2),
unit: product.rateUnit,
waterNeeded: waterNeeded.toFixed(1)
};
}
});
return {
totalArea: totalArea.toLocaleString(),
calculations
};
};
// 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,
products: selectedProducts,
plannedDate,
notes
};
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="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') {
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>
{/* 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) {
const defaultRate = selectedProduct.rates[0];
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') {
updateProduct(index, 'rateUnit', 'lb/1000sqft');
} else if (selectedProduct.productType === 'liquid') {
updateProduct(index, 'rateUnit', 'oz/1000sqft');
}
}
// For granular products, check if spreader settings exist
if (selectedProduct.productType === 'granular' && 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' ? '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' ? (
<>
<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>
{calc.waterNeeded > 0 && <p className="ml-2">Water needed: {calc.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;