diff --git a/backend/src/utils/validation.js b/backend/src/utils/validation.js index 7a5692e..3a3e456 100644 --- a/backend/src/utils/validation.js +++ b/backend/src/utils/validation.js @@ -95,10 +95,10 @@ const applicationPlanSchema = Joi.object({ lawnSectionId: Joi.number().integer().positive().required(), equipmentId: Joi.number().integer().positive().required(), plannedDate: Joi.date().required(), - notes: Joi.string(), + notes: Joi.string().allow('').optional(), products: Joi.array().items(Joi.object({ - productId: Joi.number().integer().positive(), - userProductId: Joi.number().integer().positive(), + productId: Joi.number().integer().positive().optional(), + userProductId: Joi.number().integer().positive().optional(), rateAmount: Joi.number().positive().required(), rateUnit: Joi.string().max(50).required() })).min(1).required() diff --git a/frontend/src/components/Maps/PropertyMap.js b/frontend/src/components/Maps/PropertyMap.js index a2f53b5..4c6e2b6 100644 --- a/frontend/src/components/Maps/PropertyMap.js +++ b/frontend/src/components/Maps/PropertyMap.js @@ -355,9 +355,27 @@ const PropertyMap = ({ {/* Existing sections */} {sections.map((section) => { - if (!section.polygonData?.coordinates?.[0]) return null; + console.log('Section:', section); + + // Handle both string and object polygon data + let polygonData = section.polygonData; + if (typeof polygonData === 'string') { + try { + polygonData = JSON.parse(polygonData); + } catch (e) { + console.error('Failed to parse polygon data:', e); + return null; + } + } + + if (!polygonData?.coordinates?.[0]) { + console.log('No coordinates found for section:', section.id); + return null; + } + + const coordinates = polygonData.coordinates[0].map(([lng, lat]) => [lat, lng]); + console.log('Mapped coordinates:', coordinates); - const coordinates = section.polygonData.coordinates[0].map(([lng, lat]) => [lat, lng]); const isInternallySelected = selectedSection?.id === section.id; const isExternallySelected = selectedSections.includes(section.id); const isSelected = isInternallySelected || isExternallySelected; diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index 78fdcf6..bc1f42b 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -6,7 +6,7 @@ import { WrenchScrewdriverIcon, CalculatorIcon } from '@heroicons/react/24/outline'; -import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI } from '../../services/api'; +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'; @@ -138,10 +138,12 @@ const Applications = () => { lawnSectionId: parseInt(areaId), equipmentId: parseInt(planData.equipmentId), plannedDate: new Date().toISOString().split('T')[0], // Default to today - notes: planData.notes, + notes: planData.notes || '', // Ensure notes is never null/undefined products: [{ - productId: planData.selectedProduct?.isShared ? parseInt(planData.selectedProduct.id) : null, - userProductId: !planData.selectedProduct?.isShared ? parseInt(planData.selectedProduct.id) : null, + ...(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' }] @@ -170,6 +172,7 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => { const [properties, setProperties] = useState([]); const [products, setProducts] = useState([]); const [equipment, setEquipment] = useState([]); + const [nozzles, setNozzles] = useState([]); const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null); const [loading, setLoading] = useState(true); const [loadingProperty, setLoadingProperty] = useState(false); @@ -181,6 +184,7 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => { selectedProduct: null, applicationType: '', // 'liquid' or 'granular' equipmentId: '', + nozzleId: '', notes: '' }); @@ -191,15 +195,17 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => { const fetchPlanningData = async () => { try { setLoading(true); - const [propertiesResponse, productsResponse, equipmentResponse] = await Promise.all([ + const [propertiesResponse, productsResponse, equipmentResponse, nozzlesResponse] = await Promise.all([ propertiesAPI.getAll(), productsAPI.getAll(), - equipmentAPI.getAll() + equipmentAPI.getAll(), + nozzlesAPI.getAll() ]); console.log('Properties response:', propertiesResponse.data); console.log('Products response:', productsResponse.data); console.log('Equipment response:', equipmentResponse.data); + console.log('Nozzles response:', nozzlesResponse.data); setProperties(propertiesResponse.data.data.properties || []); // Combine shared and user products with unique IDs @@ -218,6 +224,7 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => { 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'); @@ -335,6 +342,7 @@ const ApplicationPlanModal = ({ onClose, onSubmit }) => { {selectedPropertyDetails.latitude && selectedPropertyDetails.longitude && (
+ {console.log('Map sections data:', selectedPropertyDetails.sections)} {
)} + {/* Nozzle Selection for Liquid Applications */} + {planData.applicationType === 'liquid' && ( +
+ + + {nozzles.length === 0 && ( +

+ No nozzles found. Add nozzles in Equipment management first. +

+ )} +
+ )} + {/* Notes */}