diff --git a/backend/src/routes/applications.js b/backend/src/routes/applications.js index 6e675c9..f636292 100644 --- a/backend/src/routes/applications.js +++ b/backend/src/routes/applications.js @@ -273,8 +273,15 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n rateAmount: parseFloat(rateAmount), rateUnit, applicationType, - equipmentData, - nozzleData + equipmentData: { + category_name: equipmentData.category_name, + spray_width_feet: equipmentData.spray_width_feet, + tank_size_gallons: equipmentData.tank_size_gallons + }, + nozzleData: nozzleData ? { + flow_rate_gpm: nozzleData.flow_rate_gpm, + spray_angle: nozzleData.spray_angle + } : null }); // Prepare equipment object for calculations @@ -544,6 +551,51 @@ router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicat } }); +// @route DELETE /api/applications/plans/:id +// @desc Delete application plan +// @access Private +router.delete('/plans/:id', validateParams(idParamSchema), async (req, res, next) => { + try { + const planId = req.params.id; + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // Check if plan belongs to user + const planCheck = await client.query( + 'SELECT id FROM application_plans WHERE id = $1 AND user_id = $2', + [planId, req.user.id] + ); + + if (planCheck.rows.length === 0) { + throw new AppError('Application plan not found', 404); + } + + // Delete plan products first (due to foreign key constraint) + await client.query('DELETE FROM application_plan_products WHERE plan_id = $1', [planId]); + + // Delete the plan + await client.query('DELETE FROM application_plans WHERE id = $1', [planId]); + + await client.query('COMMIT'); + + res.json({ + success: true, + message: 'Application plan deleted successfully' + }); + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } catch (error) { + next(error); + } +}); + // @route PUT /api/applications/plans/:id/status // @desc Update application plan status // @access Private diff --git a/backend/src/utils/applicationCalculations.js b/backend/src/utils/applicationCalculations.js index 6ba9149..51b4b8f 100644 --- a/backend/src/utils/applicationCalculations.js +++ b/backend/src/utils/applicationCalculations.js @@ -24,7 +24,7 @@ function calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipm console.log(`Calculating liquid application: Area: ${areaSquareFeet} sq ft (${areaAcres.toFixed(3)} acres, ${area1000sqft.toFixed(1)} x 1000sqft) Rate: ${rateAmount} ${rateUnit} - Equipment: ${equipment?.categoryName} + Equipment: ${equipment?.categoryName} (width: ${equipment?.sprayWidthFeet || 'N/A'} ft) Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`); // Calculate application speed first based on equipment and nozzle diff --git a/frontend/src/pages/Applications/Applications.js b/frontend/src/pages/Applications/Applications.js index 379b95d..6685e8f 100644 --- a/frontend/src/pages/Applications/Applications.js +++ b/frontend/src/pages/Applications/Applications.js @@ -4,7 +4,9 @@ import { MapPinIcon, BeakerIcon, WrenchScrewdriverIcon, - CalculatorIcon + CalculatorIcon, + PencilIcon, + TrashIcon } from '@heroicons/react/24/outline'; import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api'; import LoadingSpinner from '../../components/UI/LoadingSpinner'; @@ -20,6 +22,7 @@ const Applications = () => { const [equipment, setEquipment] = useState([]); const [nozzles, setNozzles] = useState([]); const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null); + const [editingPlan, setEditingPlan] = useState(null); useEffect(() => { fetchApplications(); @@ -86,6 +89,34 @@ const Applications = () => { } }; + 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 (
@@ -185,6 +216,22 @@ const Applications = () => {

Created {new Date(application.createdAt).toLocaleDateString()}

+
+ + +
@@ -195,34 +242,33 @@ const Applications = () => { {/* Plan Application Modal */} {showPlanForm && ( setShowPlanForm(false)} + onClose={() => { + setShowPlanForm(false); + setEditingPlan(null); + }} properties={properties} products={products} equipment={equipment} nozzles={nozzles} selectedPropertyDetails={selectedPropertyDetails} onPropertySelect={fetchPropertyDetails} + editingPlan={editingPlan} onSubmit={async (planData) => { try { - // Create a plan for each selected area - const planPromises = planData.selectedAreas.map(async (areaId) => { - // Get the area details for calculations - const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId); + if (editingPlan) { + // Edit existing plan + const selectedArea = selectedPropertyDetails.sections.find(s => s.id === planData.selectedAreas[0]); const areaSquareFeet = selectedArea?.area || 0; - - // Get equipment details for calculations const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); - - // Get nozzle details if selected const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; const planPayload = { - lawnSectionId: parseInt(areaId), + lawnSectionId: parseInt(planData.selectedAreas[0]), equipmentId: parseInt(planData.equipmentId), nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null, - plannedDate: new Date().toISOString().split('T')[0], // Default to today + plannedDate: planData.plannedDate || new Date().toISOString().split('T')[0], notes: planData.notes || '', - areaSquareFeet: areaSquareFeet, // Pass area for calculations + areaSquareFeet: areaSquareFeet, equipment: { id: selectedEquipment?.id, categoryName: selectedEquipment?.categoryName, @@ -248,16 +294,61 @@ const Applications = () => { }] }; - return applicationsAPI.createPlan(planPayload); - }); + await applicationsAPI.updatePlan(editingPlan.id, planPayload); + toast.success('Application plan updated successfully'); + } else { + // Create new plan(s) + const planPromises = planData.selectedAreas.map(async (areaId) => { + const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId); + const areaSquareFeet = selectedArea?.area || 0; + const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId)); + const selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null; + + const planPayload = { + lawnSectionId: parseInt(areaId), + equipmentId: parseInt(planData.equipmentId), + nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null, + plannedDate: new Date().toISOString().split('T')[0], + notes: planData.notes || '', + areaSquareFeet: areaSquareFeet, + equipment: { + id: selectedEquipment?.id, + categoryName: selectedEquipment?.categoryName, + tankSizeGallons: selectedEquipment?.tankSizeGallons, + pumpGpm: selectedEquipment?.pumpGpm, + sprayWidthFeet: selectedEquipment?.sprayWidthFeet, + capacityLbs: selectedEquipment?.capacityLbs, + spreadWidth: selectedEquipment?.spreadWidth + }, + nozzle: selectedNozzle ? { + id: selectedNozzle.id, + flowRateGpm: selectedNozzle.flowRateGpm, + sprayAngle: selectedNozzle.sprayAngle + } : null, + products: [{ + ...(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 + }] + }; + + return applicationsAPI.createPlan(planPayload); + }); + + await Promise.all(planPromises); + toast.success(`Created ${planData.selectedAreas.length} application plan(s) successfully`); + } - await Promise.all(planPromises); - toast.success(`Created ${planData.selectedAreas.length} application plan(s) successfully`); setShowPlanForm(false); + setEditingPlan(null); fetchApplications(); } catch (error) { - console.error('Failed to create application plans:', error); - toast.error('Failed to create application plans'); + console.error('Failed to save application plan:', error); + toast.error('Failed to save application plan'); } }} /> @@ -275,7 +366,8 @@ const ApplicationPlanModal = ({ equipment, nozzles, selectedPropertyDetails, - onPropertySelect + onPropertySelect, + editingPlan }) => { const [loadingProperty, setLoadingProperty] = useState(false); @@ -287,9 +379,43 @@ const ApplicationPlanModal = ({ applicationType: '', // 'liquid' or 'granular' equipmentId: '', nozzleId: '', + plannedDate: '', notes: '' }); + // Initialize form with editing data + useEffect(() => { + if (editingPlan) { + const propertyId = editingPlan.section?.propertyId || editingPlan.property?.id; + if (propertyId && propertyId !== planData.propertyId) { + onPropertySelect(propertyId); + } + + // 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}`); + } + } + + 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 || '' + }); + } + }, [editingPlan, products, onPropertySelect]); + const handlePropertyChange = async (propertyId) => { setPlanData({ ...planData, propertyId, selectedAreas: [] }); if (propertyId) { @@ -342,7 +468,9 @@ const ApplicationPlanModal = ({ return (
-

Plan Application

+

+ {editingPlan ? 'Edit Application Plan' : 'Plan Application'} +

{/* Property Selection */} @@ -551,6 +679,17 @@ const ApplicationPlanModal = ({
)} + {/* Planned Date */} +
+ + setPlanData({ ...planData, plannedDate: e.target.value })} + /> +
+ {/* Notes */}
@@ -565,7 +704,7 @@ const ApplicationPlanModal = ({