diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index efffe2b..6bcd46e 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -216,12 +216,14 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) = `SELECT ap.*, ls.name as section_name, ls.area as section_area, ls.polygon_data, p.id as property_id, p.name as property_name, p.address as property_address, ue.id as equipment_id, ue.custom_name as equipment_name, - et.name as equipment_type, et.category as equipment_category + et.name as equipment_type, et.category as equipment_category, + nz.id as nozzle_id, nz.custom_name as nozzle_name, nz.flow_rate_gpm, nz.spray_angle FROM application_plans ap JOIN lawn_sections ls ON ap.lawn_section_id = ls.id JOIN properties p ON ls.property_id = p.id LEFT JOIN user_equipment ue ON ap.equipment_id = ue.id LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id + LEFT JOIN user_equipment nz ON ap.nozzle_id = nz.id WHERE ap.id = $1 AND ap.user_id = $2`, [planId, req.user.id] ); @@ -271,6 +273,12 @@ router.get('/plans/:id', validateParams(idParamSchema), async (req, res, next) = type: plan.equipment_type, category: plan.equipment_category }, + nozzle: plan.nozzle_id ? { + id: plan.nozzle_id, + name: plan.nozzle_name, + flowRateGpm: plan.flow_rate_gpm, + sprayAngle: plan.spray_angle + } : null, products: productsResult.rows.map(product => ({ id: product.id, productId: product.product_id, diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index c740821..5b81d48 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -536,6 +536,19 @@ const ApplicationPlanModal = ({ 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: '', @@ -550,33 +563,87 @@ const ApplicationPlanModal = ({ 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; - // Find the product from the plans products array - const planProduct = editingPlan.products?.[0]; - let selectedProduct = null; - if (planProduct) { - if (planProduct.productId) { - selectedProduct = products.find(p => p.uniqueId === `shared_${planProduct.productId}`); - } else if (planProduct.userProductId) { - selectedProduct = products.find(p => p.uniqueId === `user_${planProduct.userProductId}`); + // 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.section?.id], - productId: selectedProduct?.uniqueId || '', - selectedProduct: selectedProduct, - applicationType: planProduct?.applicationType || '', - equipmentId: editingPlan.equipment?.id?.toString() || '', - nozzleId: editingPlan.nozzle?.id?.toString() || '', - plannedDate: editingPlan.plannedDate ? new Date(editingPlan.plannedDate).toISOString().split('T')[0] : '', - notes: editingPlan.notes || '' - }); + setPlanData({ + propertyId: propertyId?.toString() || '', + selectedAreas: [editingPlan.section?.id], + 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.section?.id], + 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)) { @@ -595,6 +662,91 @@ const ApplicationPlanModal = ({ } }; + // 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 => { @@ -798,6 +950,10 @@ const ApplicationPlanModal = ({ 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 > @@ -951,7 +1107,14 @@ const ApplicationPlanModal = ({ setNewSpreaderSetting({ + ...newSpreaderSetting, + settingValue: e.target.value + })} + placeholder="e.g., 3.5, B, 4" + required + /> + + +
+ + setNewSpreaderSetting({ + ...newSpreaderSetting, + rateDescription: e.target.value + })} + placeholder="e.g., 2 lbs per 1000 sq ft" + /> +
+ +
+ +