1293 lines
54 KiB
JavaScript
1293 lines
54 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
PlusIcon,
|
|
MapPinIcon,
|
|
BeakerIcon,
|
|
WrenchScrewdriverIcon,
|
|
CalculatorIcon,
|
|
PencilIcon,
|
|
TrashIcon
|
|
} from '@heroicons/react/24/outline';
|
|
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
|
import PropertyMap from '../../components/Maps/PropertyMap';
|
|
import toast from 'react-hot-toast';
|
|
|
|
const Applications = () => {
|
|
const [showPlanForm, setShowPlanForm] = useState(false);
|
|
const [applications, setApplications] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [properties, setProperties] = useState([]);
|
|
const [products, setProducts] = useState([]);
|
|
const [equipment, setEquipment] = useState([]);
|
|
const [nozzles, setNozzles] = useState([]);
|
|
const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null);
|
|
const [editingPlan, setEditingPlan] = useState(null);
|
|
const [propertyCache, setPropertyCache] = useState({});
|
|
const [spreaderRecommendation, setSpreaderRecommendation] = useState(null);
|
|
const [loadingRecommendation, setLoadingRecommendation] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchApplications();
|
|
fetchPlanningData();
|
|
}, []);
|
|
|
|
const fetchApplications = async () => {
|
|
try {
|
|
const response = await applicationsAPI.getPlans();
|
|
setApplications(response.data.data.plans || []);
|
|
} catch (error) {
|
|
console.error('Failed to fetch applications:', error);
|
|
toast.error('Failed to load applications');
|
|
}
|
|
};
|
|
|
|
const fetchPlanningData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [propertiesResponse, productsResponse, equipmentResponse, nozzlesResponse] = await Promise.all([
|
|
propertiesAPI.getAll(),
|
|
productsAPI.getAll(),
|
|
equipmentAPI.getAll(),
|
|
nozzlesAPI.getAll()
|
|
]);
|
|
|
|
setProperties(propertiesResponse.data.data.properties || []);
|
|
// Combine shared and user products with unique IDs
|
|
const sharedProducts = (productsResponse.data.data.sharedProducts || []).map(product => ({
|
|
...product,
|
|
uniqueId: `shared_${product.id}`,
|
|
isShared: true
|
|
}));
|
|
|
|
const userProducts = (productsResponse.data.data.userProducts || []).map(product => ({
|
|
...product,
|
|
uniqueId: `user_${product.id}`,
|
|
isShared: false
|
|
}));
|
|
|
|
const allProducts = [...sharedProducts, ...userProducts];
|
|
setProducts(allProducts);
|
|
setEquipment(equipmentResponse.data.data.equipment || []);
|
|
setNozzles(nozzlesResponse.data.data?.nozzles || nozzlesResponse.data || []);
|
|
} catch (error) {
|
|
console.error('Failed to fetch planning data:', error);
|
|
toast.error('Failed to load planning data');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchPropertyDetails = async (propertyId) => {
|
|
// Check cache first
|
|
if (propertyCache[propertyId]) {
|
|
setSelectedPropertyDetails(propertyCache[propertyId]);
|
|
return propertyCache[propertyId];
|
|
}
|
|
|
|
try {
|
|
const response = await propertiesAPI.getById(parseInt(propertyId));
|
|
const property = response.data.data.property;
|
|
|
|
// Cache the result
|
|
setPropertyCache(prev => ({ ...prev, [propertyId]: property }));
|
|
setSelectedPropertyDetails(property);
|
|
return property;
|
|
} catch (error) {
|
|
console.error('Failed to fetch property details:', error);
|
|
toast.error('Failed to load property details');
|
|
setSelectedPropertyDetails(null);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const handleDeletePlan = async (planId, planName) => {
|
|
if (window.confirm(`Are you sure you want to delete the plan for "${planName}"?`)) {
|
|
try {
|
|
await applicationsAPI.deletePlan(planId);
|
|
toast.success('Application plan deleted successfully');
|
|
fetchApplications(); // Refresh the list
|
|
} catch (error) {
|
|
console.error('Failed to delete plan:', error);
|
|
toast.error('Failed to delete application plan');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleEditPlan = async (planId) => {
|
|
try {
|
|
// Fetch the full plan details
|
|
const response = await applicationsAPI.getPlan(planId);
|
|
const plan = response.data.data.plan;
|
|
|
|
// Set up the editing plan data
|
|
setEditingPlan(plan);
|
|
setShowPlanForm(true);
|
|
} catch (error) {
|
|
console.error('Failed to fetch plan details:', error);
|
|
toast.error('Failed to load plan details');
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex justify-center items-center h-64">
|
|
<LoadingSpinner size="lg" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Load spreader recommendations when granular product and spreader are selected
|
|
const loadSpreaderRecommendation = async (product, equipmentId, selectedAreas) => {
|
|
if (!product || !equipmentId || !selectedAreas.length || product.productType !== 'granular') {
|
|
setSpreaderRecommendation(null);
|
|
return;
|
|
}
|
|
|
|
setLoadingRecommendation(true);
|
|
try {
|
|
// Find the selected equipment details
|
|
const selectedEquipment = equipment.find(eq => eq.id === parseInt(equipmentId));
|
|
if (!selectedEquipment) {
|
|
setSpreaderRecommendation(null);
|
|
return;
|
|
}
|
|
|
|
// Load spreader settings for this product
|
|
const productApiId = product.isShared ? product.id : product.id; // Use the actual product ID
|
|
const endpoint = product.isShared
|
|
? `/api/product-spreader-settings/product/${productApiId}`
|
|
: `/api/product-spreader-settings/user-product/${productApiId}`;
|
|
|
|
const response = await fetch(endpoint, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('authToken')}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
setSpreaderRecommendation(null);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
const settings = data.data?.settings || [];
|
|
|
|
// Find a matching setting for this equipment
|
|
let matchingSetting = null;
|
|
|
|
// First try to find exact equipment match
|
|
matchingSetting = settings.find(setting =>
|
|
setting.equipmentId === selectedEquipment.id
|
|
);
|
|
|
|
// If no exact match, try to find by equipment brand/manufacturer
|
|
if (!matchingSetting && selectedEquipment.manufacturer) {
|
|
matchingSetting = settings.find(setting =>
|
|
setting.spreaderBrand &&
|
|
setting.spreaderBrand.toLowerCase().includes(selectedEquipment.manufacturer.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// If still no match, use any available setting as fallback
|
|
if (!matchingSetting && settings.length > 0) {
|
|
matchingSetting = settings[0];
|
|
}
|
|
|
|
if (matchingSetting) {
|
|
// Calculate total area and product amount needed
|
|
const totalArea = selectedAreas.reduce((sum, areaId) => {
|
|
const area = selectedPropertyDetails?.sections?.find(s => s.id === areaId);
|
|
return sum + (area?.area || 0);
|
|
}, 0);
|
|
|
|
// Calculate product amount based on rate
|
|
const rateAmount = product.customRateAmount || product.rateAmount || 1;
|
|
const rateUnit = product.customRateUnit || product.rateUnit || 'lbs/1000 sq ft';
|
|
|
|
let productAmountLbs = 0;
|
|
if (rateUnit.includes('1000')) {
|
|
// Rate per 1000 sq ft
|
|
productAmountLbs = (rateAmount * totalArea) / 1000;
|
|
} else if (rateUnit.includes('acre')) {
|
|
// Rate per acre (43,560 sq ft)
|
|
productAmountLbs = (rateAmount * totalArea) / 43560;
|
|
} else {
|
|
// Assume rate per sq ft
|
|
productAmountLbs = rateAmount * totalArea;
|
|
}
|
|
|
|
setSpreaderRecommendation({
|
|
setting: matchingSetting,
|
|
equipment: selectedEquipment,
|
|
totalArea,
|
|
productAmountLbs: Math.round(productAmountLbs * 100) / 100, // Round to 2 decimal places
|
|
isExactMatch: settings.some(s => s.equipmentId === selectedEquipment.id)
|
|
});
|
|
} else {
|
|
setSpreaderRecommendation(null);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load spreader recommendation:', error);
|
|
setSpreaderRecommendation(null);
|
|
} finally {
|
|
setLoadingRecommendation(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Applications</h1>
|
|
<p className="text-gray-600">Plan, track, and log your lawn applications</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowPlanForm(true)}
|
|
className="btn-primary flex items-center gap-2"
|
|
>
|
|
<PlusIcon className="h-5 w-5" />
|
|
Plan Application
|
|
</button>
|
|
</div>
|
|
|
|
{/* Applications List */}
|
|
{applications.length === 0 ? (
|
|
<div className="card text-center py-12">
|
|
<CalculatorIcon className="h-16 w-16 text-gray-300 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No Applications Yet</h3>
|
|
<p className="text-gray-600 mb-6">
|
|
Start by planning your first lawn application
|
|
</p>
|
|
<button
|
|
onClick={() => setShowPlanForm(true)}
|
|
className="btn-primary"
|
|
>
|
|
Plan Your First Application
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{applications.map((application) => (
|
|
<div key={application.id} className="card">
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h3 className="font-semibold text-gray-900">
|
|
{application.propertyName} - {application.sectionName}
|
|
</h3>
|
|
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
|
|
application.status === 'planned' ? 'bg-blue-100 text-blue-800' :
|
|
application.status === 'completed' ? 'bg-green-100 text-green-800' :
|
|
application.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{application.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-1">
|
|
<MapPinIcon className="h-4 w-4 inline mr-1" />
|
|
{application.propertyAddress}
|
|
</p>
|
|
<p className="text-sm text-gray-600 mb-1">
|
|
Areas: {application.sectionNames} ({Math.round(application.totalSectionArea || application.sectionArea || 0).toLocaleString()} sq ft)
|
|
</p>
|
|
<p className="text-sm text-gray-600 mb-1">
|
|
Equipment: {application.equipmentName}
|
|
</p>
|
|
<p className="text-sm text-gray-600 mb-1">
|
|
Products: {application.productCount}
|
|
</p>
|
|
{/* Display calculated amounts */}
|
|
{(application.totalProductAmount > 0 || (application.productDetails && application.productDetails.length > 0)) && (
|
|
<div className="text-sm text-green-600 mt-2 space-y-1">
|
|
<p className="font-medium">Calculated Requirements:</p>
|
|
|
|
{/* Show individual products for liquid tank mix */}
|
|
{application.productDetails && application.productDetails.length > 1 ? (
|
|
<>
|
|
{application.productDetails.map((product, index) => (
|
|
<p key={index}>
|
|
• {product.name}{product.brand ? ` (${product.brand})` : ''}: {product.calculatedAmount.toFixed(2)} oz
|
|
</p>
|
|
))}
|
|
{application.totalWaterAmount > 0 && (
|
|
<p>• Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
|
|
)}
|
|
{application.avgSpeedMph > 0 && (
|
|
<p>• Target Speed: {application.avgSpeedMph.toFixed(1)} mph</p>
|
|
)}
|
|
{application.spreaderSetting && (
|
|
<p>• Spreader Setting: {application.spreaderSetting}</p>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* Show single product with name */}
|
|
{application.productDetails && application.productDetails.length === 1 ? (
|
|
<p>• {application.productDetails[0].name}{application.productDetails[0].brand ? ` (${application.productDetails[0].brand})` : ''}: {application.productDetails[0].calculatedAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
|
|
) : (
|
|
<p>• Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
|
|
)}
|
|
{application.totalWaterAmount > 0 && (
|
|
<p>• Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
|
|
)}
|
|
{application.avgSpeedMph > 0 && (
|
|
<p>• Target Speed: {application.avgSpeedMph.toFixed(1)} mph</p>
|
|
)}
|
|
{application.spreaderSetting && (
|
|
<p>• Spreader Setting: {application.spreaderSetting}</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
{application.notes && (
|
|
<p className="text-sm text-gray-500 mt-2 italic">
|
|
"{application.notes}"
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium text-gray-900">
|
|
{application.plannedDate ? new Date(application.plannedDate).toLocaleDateString() : 'No date set'}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
Created {new Date(application.createdAt).toLocaleDateString()}
|
|
</p>
|
|
<div className="flex gap-2 mt-2">
|
|
<button
|
|
onClick={() => handleEditPlan(application.id)}
|
|
className="p-1 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded"
|
|
title="Edit plan"
|
|
>
|
|
<PencilIcon className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeletePlan(application.id, `${application.propertyName} - ${application.sectionName}`)}
|
|
className="p-1 text-red-600 hover:text-red-800 hover:bg-red-50 rounded"
|
|
title="Delete plan"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Plan Application Modal */}
|
|
{showPlanForm && (
|
|
<ApplicationPlanModal
|
|
onClose={() => {
|
|
setShowPlanForm(false);
|
|
setEditingPlan(null);
|
|
setSelectedPropertyDetails(null);
|
|
}}
|
|
properties={properties}
|
|
products={products}
|
|
equipment={equipment}
|
|
nozzles={nozzles}
|
|
selectedPropertyDetails={selectedPropertyDetails}
|
|
onPropertySelect={fetchPropertyDetails}
|
|
editingPlan={editingPlan}
|
|
onSubmit={async (planData) => {
|
|
try {
|
|
if (editingPlan) {
|
|
// Edit existing plan - backend now supports multiple areas natively
|
|
const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId));
|
|
const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null;
|
|
|
|
// Calculate total area for all selected areas
|
|
const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => {
|
|
const area = selectedPropertyDetails.sections.find(s => s.id === areaId);
|
|
return total + (area?.area || 0);
|
|
}, 0);
|
|
|
|
const planPayload = {
|
|
lawnSectionIds: planData.selectedAreas.map(id => parseInt(id)), // Send multiple areas
|
|
equipmentId: parseInt(planData.equipmentId),
|
|
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
|
|
plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0],
|
|
notes: planData.notes || '',
|
|
areaSquareFeet: totalAreaSquareFeet,
|
|
equipment: {
|
|
id: selectedEquipment?.id,
|
|
categoryName: selectedEquipment?.categoryName,
|
|
tankSizeGallons: selectedEquipment?.tankSizeGallons,
|
|
pumpGpm: selectedEquipment?.pumpGpm,
|
|
sprayWidthFeet: selectedEquipment?.sprayWidthFeet,
|
|
capacityLbs: selectedEquipment?.capacityLbs,
|
|
spreadWidth: selectedEquipment?.spreadWidth
|
|
},
|
|
...(planData.applicationType === 'liquid' && selectedNozzle && {
|
|
nozzle: {
|
|
id: selectedNozzle.id,
|
|
flowRateGpm: selectedNozzle.flowRateGpm,
|
|
sprayAngle: selectedNozzle.sprayAngle
|
|
}
|
|
}),
|
|
products: planData.applicationType === 'liquid'
|
|
? planData.selectedProducts.map(item => ({
|
|
...(item.product?.isShared
|
|
? { productId: parseInt(item.product.id) }
|
|
: { userProductId: parseInt(item.product.id) }
|
|
),
|
|
rateAmount: parseFloat(item.rateAmount || 1),
|
|
rateUnit: item.rateUnit || 'oz/1000 sq ft',
|
|
applicationType: planData.applicationType
|
|
}))
|
|
: [{
|
|
...(planData.selectedProduct?.isShared
|
|
? { productId: parseInt(planData.selectedProduct.id) }
|
|
: { userProductId: parseInt(planData.selectedProduct.id) }
|
|
),
|
|
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
|
|
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft',
|
|
applicationType: planData.applicationType
|
|
}]
|
|
};
|
|
|
|
await applicationsAPI.updatePlan(editingPlan.id, planPayload);
|
|
toast.success(`Application plan updated successfully for ${planData.selectedAreas.length} area(s)`);
|
|
} else {
|
|
// Create new plan - backend now supports multiple areas in single plan
|
|
const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId));
|
|
const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null;
|
|
|
|
// Calculate total area for all selected areas
|
|
const totalAreaSquareFeet = planData.selectedAreas.reduce((total, areaId) => {
|
|
const area = selectedPropertyDetails.sections.find(s => s.id === areaId);
|
|
return total + (area?.area || 0);
|
|
}, 0);
|
|
|
|
const planPayload = {
|
|
lawnSectionIds: planData.selectedAreas.map(id => parseInt(id)), // Send multiple areas
|
|
equipmentId: parseInt(planData.equipmentId),
|
|
...(planData.applicationType === 'liquid' && planData.nozzleId && { nozzleId: parseInt(planData.nozzleId) }),
|
|
plannedDate: new Date().toISOString().split('T')[0],
|
|
notes: planData.notes || '',
|
|
areaSquareFeet: totalAreaSquareFeet,
|
|
equipment: {
|
|
id: selectedEquipment?.id,
|
|
categoryName: selectedEquipment?.categoryName,
|
|
tankSizeGallons: selectedEquipment?.tankSizeGallons,
|
|
pumpGpm: selectedEquipment?.pumpGpm,
|
|
sprayWidthFeet: selectedEquipment?.sprayWidthFeet,
|
|
capacityLbs: selectedEquipment?.capacityLbs,
|
|
spreadWidth: selectedEquipment?.spreadWidth
|
|
},
|
|
...(planData.applicationType === 'liquid' && selectedNozzle && {
|
|
nozzle: {
|
|
id: selectedNozzle.id,
|
|
flowRateGpm: selectedNozzle.flowRateGpm,
|
|
sprayAngle: selectedNozzle.sprayAngle
|
|
}
|
|
}),
|
|
products: planData.applicationType === 'liquid'
|
|
? planData.selectedProducts.map(item => ({
|
|
...(item.product?.isShared
|
|
? { productId: parseInt(item.product.id) }
|
|
: { userProductId: parseInt(item.product.id) }
|
|
),
|
|
rateAmount: parseFloat(item.rateAmount || 1),
|
|
rateUnit: item.rateUnit || 'oz/1000 sq ft',
|
|
applicationType: planData.applicationType
|
|
}))
|
|
: [{
|
|
...(planData.selectedProduct?.isShared
|
|
? { productId: parseInt(planData.selectedProduct.id) }
|
|
: { userProductId: parseInt(planData.selectedProduct.id) }
|
|
),
|
|
rateAmount: parseFloat(planData.selectedProduct?.customRateAmount || planData.selectedProduct?.rateAmount || 1),
|
|
rateUnit: planData.selectedProduct?.customRateUnit || planData.selectedProduct?.rateUnit || 'per 1000sqft',
|
|
applicationType: planData.applicationType
|
|
}]
|
|
};
|
|
|
|
await applicationsAPI.createPlan(planPayload);
|
|
toast.success(`Created application plan for ${planData.selectedAreas.length} area(s) successfully`);
|
|
}
|
|
|
|
setShowPlanForm(false);
|
|
setEditingPlan(null);
|
|
fetchApplications();
|
|
} catch (error) {
|
|
console.error('Failed to save application plan:', error);
|
|
toast.error('Failed to save application plan');
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 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]);
|
|
|
|
// 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];
|
|
const applicationType = firstProduct?.productType === 'granular' ? 'granular' : 'liquid';
|
|
|
|
// 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 ||
|
|
(product.productType !== 'granular' && product.customProductType !== 'granular')) {
|
|
return;
|
|
}
|
|
|
|
const selectedEquipment = equipment.find(eq => eq.id === parseInt(equipmentId));
|
|
if (!selectedEquipment || selectedEquipment.categoryName !== 'Spreader') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Check if spreader setting exists
|
|
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}`;
|
|
|
|
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.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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Applications; |