app plan
This commit is contained in:
@@ -19,6 +19,7 @@ const ApplicationPlanModal = ({
|
|||||||
const [selectedEquipmentId, setSelectedEquipmentId] = useState('');
|
const [selectedEquipmentId, setSelectedEquipmentId] = useState('');
|
||||||
const [selectedNozzleId, setSelectedNozzleId] = useState('');
|
const [selectedNozzleId, setSelectedNozzleId] = useState('');
|
||||||
const [selectedProducts, setSelectedProducts] = useState([]);
|
const [selectedProducts, setSelectedProducts] = useState([]);
|
||||||
|
const [applicationType, setApplicationType] = useState('');
|
||||||
const [plannedDate, setPlannedDate] = useState('');
|
const [plannedDate, setPlannedDate] = useState('');
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -105,17 +106,42 @@ const ApplicationPlanModal = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add product to plan
|
// Add product to plan (for liquid tank mixes)
|
||||||
const addProduct = () => {
|
const addProduct = () => {
|
||||||
setSelectedProducts(prev => [...prev, {
|
setSelectedProducts(prev => [...prev, {
|
||||||
|
uniqueId: '',
|
||||||
productId: null,
|
productId: null,
|
||||||
userProductId: null,
|
userProductId: null,
|
||||||
|
productName: '',
|
||||||
|
productBrand: '',
|
||||||
|
productType: applicationType,
|
||||||
rateAmount: '',
|
rateAmount: '',
|
||||||
rateUnit: 'oz/1000sqft',
|
rateUnit: applicationType === 'granular' ? 'lb/1000sqft' : 'oz/1000sqft',
|
||||||
isUserProduct: false
|
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
|
// Remove product from plan
|
||||||
const removeProduct = (index) => {
|
const removeProduct = (index) => {
|
||||||
setSelectedProducts(prev => prev.filter((_, i) => i !== index));
|
setSelectedProducts(prev => prev.filter((_, i) => i !== index));
|
||||||
@@ -245,7 +271,7 @@ const ApplicationPlanModal = ({
|
|||||||
<option value="">Select equipment</option>
|
<option value="">Select equipment</option>
|
||||||
{equipment.map(eq => (
|
{equipment.map(eq => (
|
||||||
<option key={eq.id} value={eq.id}>
|
<option key={eq.id} value={eq.id}>
|
||||||
{eq.name} ({eq.type})
|
{eq.name || 'Unnamed Equipment'} {eq.type ? `(${eq.type})` : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -286,78 +312,145 @@ const ApplicationPlanModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Application Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
<BeakerIcon className="h-4 w-4 inline mr-1" />
|
||||||
|
Application Type
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-4 mb-3">
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* Products */}
|
{/* Products */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
<BeakerIcon className="h-4 w-4 inline mr-1" />
|
Products to Apply
|
||||||
Products
|
|
||||||
</label>
|
</label>
|
||||||
<button
|
{applicationType === 'liquid' && (
|
||||||
type="button"
|
<button
|
||||||
onClick={addProduct}
|
type="button"
|
||||||
className="text-blue-600 hover:text-blue-800 text-sm"
|
onClick={addProduct}
|
||||||
>
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
+ Add Product
|
>
|
||||||
</button>
|
+ Add Product (Tank Mix)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedProducts.map((product, index) => (
|
{selectedProducts.map((product, index) => (
|
||||||
<div key={index} className="border rounded p-3 mb-2">
|
<div key={index} className="border rounded p-3 mb-3">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
|
{/* Product Selection */}
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
<select
|
<select
|
||||||
value={product.isUserProduct ? `user_${product.userProductId}` : `shared_${product.productId}`}
|
value={product.uniqueId || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const [type, id] = e.target.value.split('_');
|
const selectedProduct = products.find(p => p.uniqueId === e.target.value);
|
||||||
updateProduct(index, 'isUserProduct', type === 'user');
|
if (selectedProduct) {
|
||||||
updateProduct(index, type === 'user' ? 'userProductId' : 'productId', parseInt(id));
|
updateProduct(index, 'uniqueId', e.target.value);
|
||||||
updateProduct(index, type === 'user' ? 'productId' : 'userProductId', null);
|
updateProduct(index, 'productId', selectedProduct.isShared ? selectedProduct.id : null);
|
||||||
|
updateProduct(index, 'userProductId', !selectedProduct.isShared ? selectedProduct.id : null);
|
||||||
|
updateProduct(index, 'isUserProduct', !selectedProduct.isShared);
|
||||||
|
updateProduct(index, 'productName', selectedProduct.name);
|
||||||
|
updateProduct(index, 'productBrand', selectedProduct.brand);
|
||||||
|
updateProduct(index, 'productType', selectedProduct.productType);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="border border-gray-300 rounded px-2 py-1"
|
className="border border-gray-300 rounded px-3 py-2"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select product</option>
|
<option value="">Select product</option>
|
||||||
{products.map(prod => (
|
{products
|
||||||
<option key={prod.uniqueId} value={prod.uniqueId}>
|
.filter(prod => !applicationType || prod.productType === applicationType)
|
||||||
{prod.name} {prod.brand && `(${prod.brand})`} {prod.isShared ? '' : '(Custom)'}
|
.map(prod => (
|
||||||
</option>
|
<option key={prod.uniqueId} value={prod.uniqueId}>
|
||||||
))}
|
{prod.name} {prod.brand && `(${prod.brand})`} {prod.isShared ? '' : '(Custom)'}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div className="flex gap-1">
|
{/* Rate Input */}
|
||||||
<input
|
<div className="grid grid-cols-2 gap-2">
|
||||||
type="number"
|
<div>
|
||||||
step="0.01"
|
<label className="block text-xs text-gray-600 mb-1">Application Rate</label>
|
||||||
placeholder="Rate"
|
<input
|
||||||
value={product.rateAmount}
|
type="number"
|
||||||
onChange={(e) => updateProduct(index, 'rateAmount', e.target.value)}
|
step="0.01"
|
||||||
className="border border-gray-300 rounded px-2 py-1 flex-1"
|
placeholder="Rate"
|
||||||
required
|
value={product.rateAmount || ''}
|
||||||
/>
|
onChange={(e) => updateProduct(index, 'rateAmount', e.target.value)}
|
||||||
<select
|
className="w-full border border-gray-300 rounded px-3 py-2"
|
||||||
value={product.rateUnit}
|
required
|
||||||
onChange={(e) => updateProduct(index, 'rateUnit', e.target.value)}
|
/>
|
||||||
className="border border-gray-300 rounded px-2 py-1"
|
</div>
|
||||||
>
|
<div>
|
||||||
<option value="oz/1000sqft">oz/1000sqft</option>
|
<label className="block text-xs text-gray-600 mb-1">Rate Unit</label>
|
||||||
<option value="lb/1000sqft">lb/1000sqft</option>
|
<select
|
||||||
<option value="fl oz/1000sqft">fl oz/1000sqft</option>
|
value={product.rateUnit || (applicationType === 'granular' ? 'lb/1000sqft' : 'oz/1000sqft')}
|
||||||
</select>
|
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>
|
</div>
|
||||||
|
|
||||||
<button
|
{/* Remove Button */}
|
||||||
type="button"
|
{applicationType === 'liquid' && selectedProducts.length > 1 && (
|
||||||
onClick={() => removeProduct(index)}
|
<div className="flex justify-end">
|
||||||
className="text-red-600 hover:text-red-800 px-2"
|
<button
|
||||||
>
|
type="button"
|
||||||
Remove
|
onClick={() => removeProduct(index)}
|
||||||
</button>
|
className="text-red-600 hover:text-red-800 text-sm"
|
||||||
|
>
|
||||||
|
Remove Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{selectedProducts.length === 0 && (
|
{selectedProducts.length === 0 && (
|
||||||
<p className="text-gray-500 text-sm italic">No products added yet</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
@@ -376,52 +469,67 @@ const ApplicationPlanModal = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Map */}
|
{/* Right Column - Areas & Map */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
{/* Area Selection with Checkboxes */}
|
||||||
Select Areas to Treat
|
{selectedPropertyDetails?.sections?.length > 0 && (
|
||||||
</label>
|
<div className="mb-4">
|
||||||
<div className="h-96 border rounded-lg overflow-hidden">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{selectedPropertyDetails ? (
|
Select Areas to Treat
|
||||||
<PropertyMap
|
</label>
|
||||||
center={mapCenter}
|
<div className="max-h-32 overflow-y-auto border rounded p-2 bg-gray-50">
|
||||||
zoom={16}
|
{selectedPropertyDetails.sections.map(section => (
|
||||||
property={selectedPropertyDetails}
|
<label key={section.id} className="flex items-center py-1 cursor-pointer hover:bg-gray-100 rounded px-2">
|
||||||
sections={selectedPropertyDetails.sections || []}
|
<input
|
||||||
selectedSections={selectedAreas}
|
type="checkbox"
|
||||||
onSectionClick={handleAreaClick}
|
checked={selectedAreas.includes(section.id)}
|
||||||
mode="select"
|
onChange={() => handleAreaClick(section)}
|
||||||
editable={false}
|
className="mr-2"
|
||||||
className="h-full w-full"
|
/>
|
||||||
/>
|
<span className="text-sm">
|
||||||
) : (
|
{section.name} ({Math.round(section.area || 0).toLocaleString()} sq ft)
|
||||||
<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>
|
|
||||||
|
|
||||||
{selectedAreas.length > 0 && selectedPropertyDetails && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm font-medium text-gray-700">Selected Areas:</p>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-1">
|
|
||||||
{selectedAreas.map(areaId => {
|
|
||||||
const area = selectedPropertyDetails.sections?.find(s => s.id === areaId);
|
|
||||||
return (
|
|
||||||
<span key={areaId} className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">
|
|
||||||
{area?.name} ({Math.round(area?.area || 0).toLocaleString()} sq ft)
|
|
||||||
</span>
|
</span>
|
||||||
);
|
</label>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-600 mt-1">
|
|
||||||
Total: {selectedAreas.reduce((total, areaId) => {
|
{selectedAreas.length > 0 && (
|
||||||
const area = selectedPropertyDetails.sections?.find(s => s.id === areaId);
|
<div className="mt-2 p-2 bg-blue-50 rounded">
|
||||||
return total + (area?.area || 0);
|
<p className="text-sm font-medium text-blue-800">
|
||||||
}, 0).toLocaleString()} sq ft
|
Total Selected: {selectedAreas.reduce((total, areaId) => {
|
||||||
</p>
|
const area = selectedPropertyDetails.sections?.find(s => s.id === areaId);
|
||||||
|
return total + (area?.area || 0);
|
||||||
|
}, 0).toLocaleString()} sq ft
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user