calcs
This commit is contained in:
@@ -42,7 +42,10 @@ router.get('/plans', async (req, res, next) => {
|
|||||||
`SELECT ap.*, ls.name as section_name, ls.area as section_area,
|
`SELECT ap.*, ls.name as section_name, ls.area as section_area,
|
||||||
p.name as property_name, p.address as property_address,
|
p.name as property_name, p.address as property_address,
|
||||||
ue.custom_name as equipment_name, et.name as equipment_type,
|
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
|
FROM application_plans ap
|
||||||
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
|
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
|
||||||
JOIN properties p ON ls.property_id = p.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,
|
propertyAddress: plan.property_address,
|
||||||
equipmentName: plan.equipment_name || plan.equipment_type,
|
equipmentName: plan.equipment_name || plan.equipment_type,
|
||||||
productCount: parseInt(plan.product_count),
|
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,
|
createdAt: plan.created_at,
|
||||||
updatedAt: plan.updated_at
|
updatedAt: plan.updated_at
|
||||||
}))
|
}))
|
||||||
@@ -207,9 +213,13 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
|
|
||||||
const section = sectionCheck.rows[0];
|
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(
|
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]
|
[equipmentId, req.user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -217,7 +227,20 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
throw new AppError('Equipment not found', 404);
|
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:', {
|
console.log('Creating plan with data:', {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
@@ -245,14 +268,38 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
// Use passed area or get from database
|
// Use passed area or get from database
|
||||||
const sectionArea = areaSquareFeet || parseFloat(section.area);
|
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
|
// Perform advanced calculations using the calculation engine
|
||||||
const calculations = calculateApplication({
|
const calculations = calculateApplication({
|
||||||
areaSquareFeet: sectionArea,
|
areaSquareFeet: sectionArea,
|
||||||
rateAmount: parseFloat(rateAmount),
|
rateAmount: parseFloat(rateAmount),
|
||||||
rateUnit,
|
rateUnit,
|
||||||
applicationType,
|
applicationType,
|
||||||
equipment,
|
equipment: equipmentForCalc,
|
||||||
nozzle
|
nozzle: nozzleForCalc
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Plan creation calculations:', calculations);
|
console.log('Plan creation calculations:', calculations);
|
||||||
@@ -282,6 +329,16 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
|
|
||||||
await client.query('COMMIT');
|
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({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Application plan created successfully',
|
message: 'Application plan created successfully',
|
||||||
@@ -291,7 +348,14 @@ router.post('/plans', validateRequest(applicationPlanSchema), async (req, res, n
|
|||||||
status: plan.status,
|
status: plan.status,
|
||||||
plannedDate: plan.planned_date,
|
plannedDate: plan.planned_date,
|
||||||
notes: plan.notes,
|
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
|
// @route PUT /api/applications/plans/:id/status
|
||||||
// @desc Update application plan status
|
// @desc Update application plan status
|
||||||
// @access Private
|
// @access Private
|
||||||
|
|||||||
@@ -156,9 +156,22 @@ const Applications = () => {
|
|||||||
<p className="text-sm text-gray-600 mb-1">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
Equipment: {application.equipmentName}
|
Equipment: {application.equipmentName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
Products: {application.productCount}
|
Products: {application.productCount}
|
||||||
</p>
|
</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 && (
|
{application.notes && (
|
||||||
<p className="text-sm text-gray-500 mt-2 italic">
|
<p className="text-sm text-gray-500 mt-2 italic">
|
||||||
"{application.notes}"
|
"{application.notes}"
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ export const applicationsAPI = {
|
|||||||
getPlans: (params) => apiClient.get('/applications/plans', { params }),
|
getPlans: (params) => apiClient.get('/applications/plans', { params }),
|
||||||
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),
|
||||||
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