again
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div className="p-6">
|
||||
@@ -185,6 +216,22 @@ const Applications = () => {
|
||||
<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>
|
||||
@@ -195,34 +242,75 @@ const Applications = () => {
|
||||
{/* Plan Application Modal */}
|
||||
{showPlanForm && (
|
||||
<ApplicationPlanModal
|
||||
onClose={() => 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
|
||||
if (editingPlan) {
|
||||
// Edit existing plan
|
||||
const selectedArea = selectedPropertyDetails.sections.find(s => s.id === planData.selectedAreas[0]);
|
||||
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(planData.selectedAreas[0]),
|
||||
equipmentId: parseInt(planData.equipmentId),
|
||||
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
|
||||
plannedDate: planData.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
|
||||
}]
|
||||
};
|
||||
|
||||
await applicationsAPI.updatePlan(editingPlan.id, planPayload);
|
||||
toast.success('Application plan updated successfully');
|
||||
} else {
|
||||
// Create new plan(s)
|
||||
const planPromises = planData.selectedAreas.map(async (areaId) => {
|
||||
// Get the area details for calculations
|
||||
const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId);
|
||||
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),
|
||||
equipmentId: parseInt(planData.equipmentId),
|
||||
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
|
||||
plannedDate: new Date().toISOString().split('T')[0], // Default to today
|
||||
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,
|
||||
@@ -253,11 +341,14 @@ const Applications = () => {
|
||||
|
||||
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 (
|
||||
<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">Plan Application</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{editingPlan ? 'Edit Application Plan' : 'Plan Application'}
|
||||
</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Property Selection */}
|
||||
@@ -551,6 +679,17 @@ const ApplicationPlanModal = ({
|
||||
</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>
|
||||
@@ -565,7 +704,7 @@ const ApplicationPlanModal = ({
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="submit" className="btn-primary flex-1">
|
||||
Create Application Plan
|
||||
{editingPlan ? 'Update Application Plan' : 'Create Application Plan'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -152,6 +152,7 @@ export const applicationsAPI = {
|
||||
getPlan: (id) => apiClient.get(`/applications/plans/${id}`),
|
||||
createPlan: (planData) => apiClient.post('/applications/plans', planData),
|
||||
updatePlan: (id, planData) => apiClient.put(`/applications/plans/${id}`, planData),
|
||||
deletePlan: (id) => apiClient.delete(`/applications/plans/${id}`),
|
||||
updatePlanStatus: (id, status) => apiClient.put(`/applications/plans/${id}/status`, { status }),
|
||||
|
||||
// Logs
|
||||
|
||||
Reference in New Issue
Block a user