watering attempt 1

This commit is contained in:
Jake Kasper
2025-09-04 12:46:56 -05:00
parent e4524432e7
commit 610131e5c2
7 changed files with 404 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ const weatherRoutes = require('./routes/weather');
const weatherPublicRoutes = require('./routes/weatherPublic');
const adminRoutes = require('./routes/admin');
const mowingRoutes = require('./routes/mowing');
const wateringRoutes = require('./routes/watering');
const { errorHandler } = require('./middleware/errorHandler');
const { authenticateToken } = require('./middleware/auth');
@@ -106,6 +107,7 @@ app.use('/api/nozzles', authenticateToken, nozzleRoutes);
app.use('/api/products', authenticateToken, productRoutes);
app.use('/api/applications', authenticateToken, applicationRoutes);
app.use('/api/mowing', authenticateToken, mowingRoutes);
app.use('/api/watering', authenticateToken, wateringRoutes);
app.use('/api/spreader-settings', authenticateToken, spreaderSettingsRoutes);
app.use('/api/product-spreader-settings', authenticateToken, productSpreaderSettingsRoutes);
app.use('/api/weather', authenticateToken, weatherRoutes);

View File

@@ -0,0 +1,105 @@
const express = require('express');
const pool = require('../config/database');
const { AppError } = require('../middleware/errorHandler');
const router = express.Router();
// Compute coverage area in sqft (server-side helper)
const computeCoverageSqft = (point) => {
const {
sprinkler_head_type,
sprinkler_throw_feet,
sprinkler_degrees,
sprinkler_length_feet,
sprinkler_width_feet
} = point;
if (sprinkler_head_type === 'rotor_impact' || sprinkler_head_type === 'spray_fixed') {
const r = Number(sprinkler_throw_feet || 0);
const deg = Math.min(360, Math.max(0, Number(sprinkler_degrees || 360)));
const area = Math.PI * r * r * (deg / 360);
return Math.round(area * 100) / 100;
}
if (sprinkler_head_type === 'oscillating_fan') {
const L = Number(sprinkler_length_feet || 0);
const W = Number(sprinkler_width_feet || 0);
const area = L * W;
return Math.round(area * 100) / 100;
}
if (sprinkler_head_type === 'drip') {
return 0; // coverage represented differently; skip for now
}
return 0;
};
// GET /api/watering/plans?property_id=...
router.get('/plans', async (req, res, next) => {
try {
const { property_id } = req.query;
const rows = await pool.query(
`SELECT * FROM watering_plans WHERE user_id=$1 ${property_id? 'AND property_id=$2':''} ORDER BY created_at DESC`,
property_id? [req.user.id, property_id] : [req.user.id]
);
res.json({ success:true, data:{ plans: rows.rows }});
} catch (e) { next(e); }
});
// POST /api/watering/plans
router.post('/plans', async (req, res, next) => {
try {
const { propertyId, name, notes } = req.body;
if (!propertyId || !name) throw new AppError('propertyId and name required', 400);
// Verify property ownership
const pr = await pool.query('SELECT id FROM properties WHERE id=$1 AND user_id=$2', [propertyId, req.user.id]);
if (pr.rows.length === 0) throw new AppError('Property not found', 404);
const ins = await pool.query(
`INSERT INTO watering_plans(user_id, property_id, name, notes) VALUES ($1,$2,$3,$4) RETURNING *`,
[req.user.id, propertyId, name, notes||null]
);
res.status(201).json({ success:true, data:{ plan: ins.rows[0] }});
} catch (e) { next(e); }
});
// GET /api/watering/plans/:id/points
router.get('/plans/:id/points', async (req, res, next) => {
try {
const planId = req.params.id;
const ch = await pool.query('SELECT id FROM watering_plans WHERE id=$1 AND user_id=$2',[planId, req.user.id]);
if (ch.rows.length===0) throw new AppError('Plan not found',404);
const rs = await pool.query('SELECT * FROM watering_plan_points WHERE plan_id=$1 ORDER BY sequence', [planId]);
res.json({ success:true, data:{ points: rs.rows }});
} catch (e) { next(e); }
});
// POST /api/watering/plans/:id/points
router.post('/plans/:id/points', async (req,res,next)=>{
try {
const planId = req.params.id;
const ch = await pool.query('SELECT id, property_id FROM watering_plans WHERE id=$1 AND user_id=$2',[planId, req.user.id]);
if (ch.rows.length===0) throw new AppError('Plan not found',404);
const seqrs = await pool.query('SELECT COALESCE(MAX(sequence),0)+1 as next FROM watering_plan_points WHERE plan_id=$1',[planId]);
const sequence = seqrs.rows[0].next;
const payload = req.body || {};
const point = {
sprinkler_head_type: payload.sprinklerHeadType,
sprinkler_throw_feet: payload.throwFeet,
sprinkler_degrees: payload.degrees,
sprinkler_length_feet: payload.lengthFeet,
sprinkler_width_feet: payload.widthFeet
};
const coverage = computeCoverageSqft(point);
const ins = await pool.query(
`INSERT INTO watering_plan_points
(plan_id, sequence, lat, lng, duration_minutes, sprinkler_mount, sprinkler_head_type,
sprinkler_gpm, sprinkler_throw_feet, sprinkler_degrees, sprinkler_length_feet, sprinkler_width_feet, coverage_sqft)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`,
[planId, sequence, payload.lat, payload.lng, payload.durationMinutes||0, payload.mountType||null,
payload.sprinklerHeadType||null, payload.gpm||null, payload.throwFeet||null, payload.degrees||null,
payload.lengthFeet||null, payload.widthFeet||null, coverage]
);
res.status(201).json({ success:true, data:{ point: ins.rows[0] }});
} catch (e) { next(e); }
});
module.exports = router;