This commit is contained in:
Jake Kasper
2025-08-23 13:53:57 -04:00
parent 647e76f643
commit 3a1734ffc0
3 changed files with 258 additions and 8 deletions

View File

@@ -42,7 +42,10 @@ router.get('/plans', async (req, res, next) => {
`SELECT ap.*, ls.name as section_name, ls.area as section_area,
p.name as property_name, p.address as property_address,
ue.custom_name as equipment_name, et.name as equipment_type,
COUNT(app.id) as product_count
COUNT(app.id) as product_count,
SUM(app.calculated_product_amount) as total_product_amount,
SUM(app.calculated_water_amount) as total_water_amount,
AVG(app.target_speed_mph) as avg_speed_mph
FROM application_plans ap
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
JOIN properties p ON ls.property_id = p.id
@@ -69,6 +72,9 @@ router.get('/plans', async (req, res, next) => {
propertyAddress: plan.property_address,
equipmentName: plan.equipment_name || plan.equipment_type,
productCount: parseInt(plan.product_count),
totalProductAmount: parseFloat(plan.total_product_amount || 0),
totalWaterAmount: parseFloat(plan.total_water_amount || 0),
avgSpeedMph: parseFloat(plan.avg_speed_mph || 0),
createdAt: plan.created_at,
updatedAt: plan.updated_at
}))
@@ -207,9 +213,13 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
const section = sectionCheck.rows[0];
// Verify equipment belongs to user
// Verify equipment belongs to user and get equipment details with category
const equipmentCheck = await client.query(
'SELECT id, tank_size, pump_gpm, nozzle_gpm, nozzle_count FROM user_equipment WHERE id = $1 AND user_id = $2',
`SELECT ue.*, et.name as type_name, ec.name as category_name
FROM user_equipment ue
LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id
LEFT JOIN equipment_categories ec ON COALESCE(et.category_id, ue.category_id) = ec.id
WHERE ue.id = $1 AND ue.user_id = $2`,
[equipmentId, req.user.id]
);
@@ -217,7 +227,20 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
throw new AppError('Equipment not found', 404);
}
const equipment = equipmentCheck.rows[0];
const equipmentData = equipmentCheck.rows[0];
// Get nozzle data if provided
let nozzleData = null;
if (nozzleId) {
const nozzleCheck = await client.query(
'SELECT * FROM user_equipment WHERE id = $1 AND user_id = $2',
[nozzleId, req.user.id]
);
if (nozzleCheck.rows.length > 0) {
nozzleData = nozzleCheck.rows[0];
}
}
console.log('Creating plan with data:', {
userId: req.user.id,
@@ -245,14 +268,38 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
// Use passed area or get from database
const sectionArea = areaSquareFeet || parseFloat(section.area);
console.log('Calculation inputs:', {
areaSquareFeet: sectionArea,
rateAmount: parseFloat(rateAmount),
rateUnit,
applicationType,
equipmentData,
nozzleData
});
// Prepare equipment object for calculations
const equipmentForCalc = {
categoryName: equipmentData.category_name,
tankSizeGallons: equipmentData.tank_size_gallons,
sprayWidthFeet: equipmentData.spray_width_feet,
capacityLbs: equipmentData.capacity_lbs,
spreadWidth: equipmentData.spread_width
};
// Prepare nozzle object for calculations
const nozzleForCalc = nozzleData ? {
flowRateGpm: nozzleData.flow_rate_gpm,
sprayAngle: nozzleData.spray_angle
} : null;
// Perform advanced calculations using the calculation engine
const calculations = calculateApplication({
areaSquareFeet: sectionArea,
rateAmount: parseFloat(rateAmount),
rateUnit,
applicationType,
equipment,
nozzle
equipment: equipmentForCalc,
nozzle: nozzleForCalc
});
console.log('Plan creation calculations:', calculations);
@@ -282,6 +329,16 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
await client.query('COMMIT');
// Get the created plan with calculations for response
const createdPlanResult = await client.query(
`SELECT ap.*, app.calculated_product_amount, app.calculated_water_amount, app.target_speed_mph,
app.rate_amount, app.rate_unit
FROM application_plans ap
LEFT JOIN application_plan_products app ON ap.id = app.plan_id
WHERE ap.id = $1`,
[plan.id]
);
res.status(201).json({
success: true,
message: 'Application plan created successfully',
@@ -291,7 +348,14 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
status: plan.status,
plannedDate: plan.planned_date,
notes: plan.notes,
createdAt: plan.created_at
createdAt: plan.created_at,
calculations: createdPlanResult.rows.map(row => ({
productAmount: row.calculated_product_amount,
waterAmount: row.calculated_water_amount,
targetSpeed: row.target_speed_mph,
rateAmount: row.rate_amount,
rateUnit: row.rate_unit
}))
}
}
});
@@ -308,6 +372,178 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
}
});
// @route PUT /api/applications/plans/:id
// @desc Update application plan
// @access Private
router.put('/plans/:id', validateParams(idParamSchema), validateRequest(applicationPlanSchema), async (req, res, next) => {
try {
const planId = req.params.id;
const {
lawnSectionId,
equipmentId,
nozzleId,
plannedDate,
notes,
products,
areaSquareFeet,
equipment,
nozzle
} = req.body;
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);
}
// Verify lawn section belongs to user
const sectionCheck = await client.query(
`SELECT ls.id, ls.area, p.user_id
FROM lawn_sections ls
JOIN properties p ON ls.property_id = p.id
WHERE ls.id = $1 AND p.user_id = $2`,
[lawnSectionId, req.user.id]
);
if (sectionCheck.rows.length === 0) {
throw new AppError('Lawn section not found', 404);
}
const section = sectionCheck.rows[0];
// Verify equipment belongs to user and get equipment details
const equipmentCheck = await client.query(
`SELECT ue.*, et.name as type_name, ec.name as category_name
FROM user_equipment ue
LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id
LEFT JOIN equipment_categories ec ON COALESCE(et.category_id, ue.category_id) = ec.id
WHERE ue.id = $1 AND ue.user_id = $2`,
[equipmentId, req.user.id]
);
if (equipmentCheck.rows.length === 0) {
throw new AppError('Equipment not found', 404);
}
const equipmentData = equipmentCheck.rows[0];
// Get nozzle data if provided
let nozzleData = null;
if (nozzleId) {
const nozzleCheck = await client.query(
'SELECT * FROM user_equipment WHERE id = $1 AND user_id = $2',
[nozzleId, req.user.id]
);
if (nozzleCheck.rows.length > 0) {
nozzleData = nozzleCheck.rows[0];
}
}
// Update application plan
const updateResult = await client.query(
`UPDATE application_plans
SET lawn_section_id = $1, equipment_id = $2, nozzle_id = $3,
planned_date = $4, notes = $5, updated_at = CURRENT_TIMESTAMP
WHERE id = $6
RETURNING *`,
[lawnSectionId, equipmentId, nozzleId, plannedDate, notes, planId]
);
const plan = updateResult.rows[0];
// Delete existing products
await client.query('DELETE FROM application_plan_products WHERE plan_id = $1', [planId]);
// Add updated products with recalculation
for (const product of products) {
const { productId, userProductId, rateAmount, rateUnit, applicationType } = product;
const sectionArea = areaSquareFeet || parseFloat(section.area);
// Prepare equipment object for calculations
const equipmentForCalc = {
categoryName: equipmentData.category_name,
tankSizeGallons: equipmentData.tank_size_gallons,
sprayWidthFeet: equipmentData.spray_width_feet,
capacityLbs: equipmentData.capacity_lbs,
spreadWidth: equipmentData.spread_width
};
// Prepare nozzle object for calculations
const nozzleForCalc = nozzleData ? {
flowRateGpm: nozzleData.flow_rate_gpm,
sprayAngle: nozzleData.spray_angle
} : null;
// Perform recalculation
const calculations = calculateApplication({
areaSquareFeet: sectionArea,
rateAmount: parseFloat(rateAmount),
rateUnit,
applicationType,
equipment: equipmentForCalc,
nozzle: nozzleForCalc
});
// Extract calculated values
let calculatedProductAmount = 0;
let calculatedWaterAmount = 0;
let targetSpeed = calculations.applicationSpeedMph || 3;
if (calculations.type === 'liquid') {
calculatedProductAmount = calculations.productAmountOunces || 0;
calculatedWaterAmount = calculations.waterAmountGallons || 0;
} else if (calculations.type === 'granular') {
calculatedProductAmount = calculations.productAmountPounds || 0;
calculatedWaterAmount = 0;
}
await client.query(
`INSERT INTO application_plan_products
(plan_id, product_id, user_product_id, rate_amount, rate_unit,
calculated_product_amount, calculated_water_amount, target_speed_mph)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[plan.id, productId, userProductId, rateAmount, rateUnit,
calculatedProductAmount, calculatedWaterAmount, targetSpeed]
);
}
await client.query('COMMIT');
res.json({
success: true,
message: 'Application plan updated successfully',
data: {
plan: {
id: plan.id,
status: plan.status,
plannedDate: plan.planned_date,
notes: plan.notes,
updatedAt: plan.updated_at
}
}
});
} 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

View File

@@ -156,9 +156,22 @@ const Applications = () => {
<p className="text-sm text-gray-600 mb-1">
Equipment: {application.equipmentName}
</p>
<p className="text-sm text-gray-600">
<p className="text-sm text-gray-600 mb-1">
Products: {application.productCount}
</p>
{/* Display calculated amounts */}
{application.totalProductAmount > 0 && (
<div className="text-sm text-green-600 mt-2 space-y-1">
<p className="font-medium">Calculated Requirements:</p>
<p> Product: {application.totalProductAmount.toFixed(2)} {application.totalWaterAmount > 0 ? 'oz' : 'lbs'}</p>
{application.totalWaterAmount > 0 && (
<p> Water: {application.totalWaterAmount.toFixed(2)} gallons</p>
)}
{application.avgSpeedMph > 0 && (
<p> Target Speed: {application.avgSpeedMph.toFixed(1)} mph</p>
)}
</div>
)}
{application.notes && (
<p className="text-sm text-gray-500 mt-2 italic">
"{application.notes}"

View File

@@ -151,6 +151,7 @@ export const applicationsAPI = {
getPlans: (params) => apiClient.get('/applications/plans', { params }),
getPlan: (id) => apiClient.get(`/applications/plans/${id}`),
createPlan: (planData) => apiClient.post('/applications/plans', planData),
updatePlan: (id, planData) => apiClient.put(`/applications/plans/${id}`, planData),
updatePlanStatus: (id, status) => apiClient.put(`/applications/plans/${id}/status`, { status }),
// Logs