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'}
+
)}
+ {/* Planned Date */}
+
+
+ setPlanData({ ...planData, plannedDate: e.target.value })}
+ />
+
+
{/* Notes */}
@@ -565,7 +704,7 @@ const ApplicationPlanModal = ({