This commit is contained in:
Jake Kasper
2025-08-23 14:06:32 -04:00
parent f12818cfdd
commit ae3ad6fb81
4 changed files with 217 additions and 25 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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