again
This commit is contained in:
@@ -273,8 +273,15 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
rateAmount: parseFloat(rateAmount),
|
rateAmount: parseFloat(rateAmount),
|
||||||
rateUnit,
|
rateUnit,
|
||||||
applicationType,
|
applicationType,
|
||||||
equipmentData,
|
equipmentData: {
|
||||||
nozzleData
|
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
|
// 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
|
// @route PUT /api/applications/plans/:id/status
|
||||||
// @desc Update application plan status
|
// @desc Update application plan status
|
||||||
// @access Private
|
// @access Private
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function calculateLiquidApplication(areaSquareFeet, rateAmount, rateUnit, equipm
|
|||||||
console.log(`Calculating liquid application:
|
console.log(`Calculating liquid application:
|
||||||
Area: ${areaSquareFeet} sq ft (${areaAcres.toFixed(3)} acres, ${area1000sqft.toFixed(1)} x 1000sqft)
|
Area: ${areaSquareFeet} sq ft (${areaAcres.toFixed(3)} acres, ${area1000sqft.toFixed(1)} x 1000sqft)
|
||||||
Rate: ${rateAmount} ${rateUnit}
|
Rate: ${rateAmount} ${rateUnit}
|
||||||
Equipment: ${equipment?.categoryName}
|
Equipment: ${equipment?.categoryName} (width: ${equipment?.sprayWidthFeet || 'N/A'} ft)
|
||||||
Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`);
|
Nozzle GPM: ${nozzle?.flowRateGpm || 'N/A'}`);
|
||||||
|
|
||||||
// Calculate application speed first based on equipment and nozzle
|
// Calculate application speed first based on equipment and nozzle
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import {
|
|||||||
MapPinIcon,
|
MapPinIcon,
|
||||||
BeakerIcon,
|
BeakerIcon,
|
||||||
WrenchScrewdriverIcon,
|
WrenchScrewdriverIcon,
|
||||||
CalculatorIcon
|
CalculatorIcon,
|
||||||
|
PencilIcon,
|
||||||
|
TrashIcon
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
import { propertiesAPI, productsAPI, equipmentAPI, applicationsAPI, nozzlesAPI } from '../../services/api';
|
||||||
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
import LoadingSpinner from '../../components/UI/LoadingSpinner';
|
||||||
@@ -20,6 +22,7 @@ const Applications = () => {
|
|||||||
const [equipment, setEquipment] = useState([]);
|
const [equipment, setEquipment] = useState([]);
|
||||||
const [nozzles, setNozzles] = useState([]);
|
const [nozzles, setNozzles] = useState([]);
|
||||||
const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null);
|
const [selectedPropertyDetails, setSelectedPropertyDetails] = useState(null);
|
||||||
|
const [editingPlan, setEditingPlan] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchApplications();
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
@@ -185,6 +216,22 @@ const Applications = () => {
|
|||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
Created {new Date(application.createdAt).toLocaleDateString()}
|
Created {new Date(application.createdAt).toLocaleDateString()}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,34 +242,33 @@ const Applications = () => {
|
|||||||
{/* Plan Application Modal */}
|
{/* Plan Application Modal */}
|
||||||
{showPlanForm && (
|
{showPlanForm && (
|
||||||
<ApplicationPlanModal
|
<ApplicationPlanModal
|
||||||
onClose={() => setShowPlanForm(false)}
|
onClose={() => {
|
||||||
|
setShowPlanForm(false);
|
||||||
|
setEditingPlan(null);
|
||||||
|
}}
|
||||||
properties={properties}
|
properties={properties}
|
||||||
products={products}
|
products={products}
|
||||||
equipment={equipment}
|
equipment={equipment}
|
||||||
nozzles={nozzles}
|
nozzles={nozzles}
|
||||||
selectedPropertyDetails={selectedPropertyDetails}
|
selectedPropertyDetails={selectedPropertyDetails}
|
||||||
onPropertySelect={fetchPropertyDetails}
|
onPropertySelect={fetchPropertyDetails}
|
||||||
|
editingPlan={editingPlan}
|
||||||
onSubmit={async (planData) => {
|
onSubmit={async (planData) => {
|
||||||
try {
|
try {
|
||||||
// Create a plan for each selected area
|
if (editingPlan) {
|
||||||
const planPromises = planData.selectedAreas.map(async (areaId) => {
|
// Edit existing plan
|
||||||
// Get the area details for calculations
|
const selectedArea = selectedPropertyDetails.sections.find(s => s.id === planData.selectedAreas[0]);
|
||||||
const selectedArea = selectedPropertyDetails.sections.find(s => s.id === areaId);
|
|
||||||
const areaSquareFeet = selectedArea?.area || 0;
|
const areaSquareFeet = selectedArea?.area || 0;
|
||||||
|
|
||||||
// Get equipment details for calculations
|
|
||||||
const selectedEquipment = equipment.find(eq => eq.id === parseInt(planData.equipmentId));
|
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 selectedNozzle = planData.nozzleId ? nozzles.find(n => n.id === parseInt(planData.nozzleId)) : null;
|
||||||
|
|
||||||
const planPayload = {
|
const planPayload = {
|
||||||
lawnSectionId: parseInt(areaId),
|
lawnSectionId: parseInt(planData.selectedAreas[0]),
|
||||||
equipmentId: parseInt(planData.equipmentId),
|
equipmentId: parseInt(planData.equipmentId),
|
||||||
nozzleId: planData.nozzleId ? parseInt(planData.nozzleId) : null,
|
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 || '',
|
notes: planData.notes || '',
|
||||||
areaSquareFeet: areaSquareFeet, // Pass area for calculations
|
areaSquareFeet: areaSquareFeet,
|
||||||
equipment: {
|
equipment: {
|
||||||
id: selectedEquipment?.id,
|
id: selectedEquipment?.id,
|
||||||
categoryName: selectedEquipment?.categoryName,
|
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);
|
setShowPlanForm(false);
|
||||||
|
setEditingPlan(null);
|
||||||
fetchApplications();
|
fetchApplications();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create application plans:', error);
|
console.error('Failed to save application plan:', error);
|
||||||
toast.error('Failed to create application plans');
|
toast.error('Failed to save application plan');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -275,7 +366,8 @@ const ApplicationPlanModal = ({
|
|||||||
equipment,
|
equipment,
|
||||||
nozzles,
|
nozzles,
|
||||||
selectedPropertyDetails,
|
selectedPropertyDetails,
|
||||||
onPropertySelect
|
onPropertySelect,
|
||||||
|
editingPlan
|
||||||
}) => {
|
}) => {
|
||||||
const [loadingProperty, setLoadingProperty] = useState(false);
|
const [loadingProperty, setLoadingProperty] = useState(false);
|
||||||
|
|
||||||
@@ -287,9 +379,43 @@ const ApplicationPlanModal = ({
|
|||||||
applicationType: '', // 'liquid' or 'granular'
|
applicationType: '', // 'liquid' or 'granular'
|
||||||
equipmentId: '',
|
equipmentId: '',
|
||||||
nozzleId: '',
|
nozzleId: '',
|
||||||
|
plannedDate: '',
|
||||||
notes: ''
|
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) => {
|
const handlePropertyChange = async (propertyId) => {
|
||||||
setPlanData({ ...planData, propertyId, selectedAreas: [] });
|
setPlanData({ ...planData, propertyId, selectedAreas: [] });
|
||||||
if (propertyId) {
|
if (propertyId) {
|
||||||
@@ -342,7 +468,9 @@ const ApplicationPlanModal = ({
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<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">
|
<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">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* Property Selection */}
|
{/* Property Selection */}
|
||||||
@@ -551,6 +679,17 @@ const ApplicationPlanModal = ({
|
|||||||
</div>
|
</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 */}
|
{/* Notes */}
|
||||||
<div>
|
<div>
|
||||||
<label className="label">Notes</label>
|
<label className="label">Notes</label>
|
||||||
@@ -565,7 +704,7 @@ const ApplicationPlanModal = ({
|
|||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<button type="submit" className="btn-primary flex-1">
|
<button type="submit" className="btn-primary flex-1">
|
||||||
Create Application Plan
|
{editingPlan ? 'Update Application Plan' : 'Create Application Plan'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const applicationsAPI = {
|
|||||||
getPlan: (id) => apiClient.get(`/applications/plans/${id}`),
|
getPlan: (id) => apiClient.get(`/applications/plans/${id}`),
|
||||||
createPlan: (planData) => apiClient.post('/applications/plans', planData),
|
createPlan: (planData) => apiClient.post('/applications/plans', planData),
|
||||||
updatePlan: (id, planData) => apiClient.put(`/applications/plans/${id}`, 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 }),
|
updatePlanStatus: (id, status) => apiClient.put(`/applications/plans/${id}/status`, { status }),
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
|
|||||||
Reference in New Issue
Block a user