This commit is contained in:
Jake Kasper
2025-09-02 09:18:41 -05:00
parent 90af8fab02
commit f29876b041
8 changed files with 389 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ const productSpreaderSettingsRoutes = require('./routes/productSpreaderSettings'
const weatherRoutes = require('./routes/weather');
const weatherPublicRoutes = require('./routes/weatherPublic');
const adminRoutes = require('./routes/admin');
const mowingRoutes = require('./routes/mowing');
const { errorHandler } = require('./middleware/errorHandler');
const { authenticateToken } = require('./middleware/auth');
@@ -100,6 +101,7 @@ app.use('/api/equipment', authenticateToken, equipmentRoutes);
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/spreader-settings', authenticateToken, spreaderSettingsRoutes);
app.use('/api/product-spreader-settings', authenticateToken, productSpreaderSettingsRoutes);
app.use('/api/weather', authenticateToken, weatherRoutes);

View File

@@ -0,0 +1,129 @@
const express = require('express');
const pool = require('../config/database');
const { AppError } = require('../middleware/errorHandler');
const { validateRequest } = require('../utils/validation');
const { mowingSessionSchema } = require('../utils/validation');
const router = express.Router();
// POST /api/mowing/sessions - create a completed mowing session
router.post('/sessions', validateRequest(mowingSessionSchema), async (req, res, next) => {
try {
const {
propertyId,
lawnSectionIds,
equipmentId,
cutHeightInches,
direction,
gpsTrack,
averageSpeed,
durationSeconds,
totalDistanceMeters,
areaCoveredSqft,
notes
} = req.body;
// Verify ownership of property, sections, equipment
const prop = await pool.query('SELECT id FROM properties WHERE id=$1 AND user_id=$2', [propertyId, req.user.id]);
if (prop.rows.length === 0) throw new AppError('Property not found', 404);
const sections = await pool.query(
`SELECT ls.id FROM lawn_sections ls JOIN properties p ON ls.property_id=p.id
WHERE ls.id = ANY($1::int[]) AND p.user_id=$2`,
[lawnSectionIds, req.user.id]
);
if (sections.rows.length !== lawnSectionIds.length) throw new AppError('One or more sections not found', 404);
const equip = await pool.query('SELECT id FROM user_equipment WHERE id=$1 AND user_id=$2', [equipmentId, req.user.id]);
if (equip.rows.length === 0) throw new AppError('Equipment not found', 404);
const client = await pool.connect();
try {
await client.query('BEGIN');
const ins = await client.query(
`INSERT INTO mowing_sessions
(user_id, property_id, equipment_id, cut_height_inches, direction, gps_track,
duration_seconds, total_distance_meters, average_speed_mph, area_covered_sqft, notes)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
RETURNING *`,
[
req.user.id,
propertyId,
equipmentId,
cutHeightInches,
direction,
JSON.stringify(gpsTrack),
durationSeconds,
totalDistanceMeters,
averageSpeed,
areaCoveredSqft || null,
notes || null
]
);
const session = ins.rows[0];
for (const sid of lawnSectionIds) {
await client.query(
`INSERT INTO mowing_session_sections (session_id, lawn_section_id) VALUES ($1,$2)`,
[session.id, sid]
);
}
await client.query('COMMIT');
return res.status(201).json({ success: true, data: { session: { id: session.id } } });
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
} catch (error) {
next(error);
}
});
// GET /api/mowing/sessions - list sessions for user
router.get('/sessions', async (req, res, next) => {
try {
const result = await pool.query(
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name
FROM mowing_sessions ms
JOIN properties p ON ms.property_id=p.id
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
WHERE ms.user_id=$1
ORDER BY ms.created_at DESC
LIMIT 200`,
[req.user.id]
);
res.json({ success: true, data: { sessions: result.rows } });
} catch (error) {
next(error);
}
});
// GET /api/mowing/sessions/:id - details
router.get('/sessions/:id', async (req, res, next) => {
try {
const { id } = req.params;
const sessionRes = await pool.query(
`SELECT ms.*, p.name as property_name, ue.custom_name as equipment_name
FROM mowing_sessions ms
JOIN properties p ON ms.property_id=p.id
LEFT JOIN user_equipment ue ON ms.equipment_id=ue.id
WHERE ms.id=$1 AND ms.user_id=$2`,
[id, req.user.id]
);
if (sessionRes.rows.length === 0) throw new AppError('Session not found', 404);
const sectionsRes = await pool.query(
`SELECT lss.lawn_section_id, ls.name, ls.area, ls.polygon_data
FROM mowing_session_sections lss
JOIN lawn_sections ls ON lss.lawn_section_id=ls.id
WHERE lss.session_id=$1`,
[id]
);
res.json({ success: true, data: { session: sessionRes.rows[0], sections: sectionsRes.rows } });
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@@ -180,6 +180,21 @@ const applicationLogSchema = Joi.object({
}).or('productId', 'userProductId')).min(1).required()
});
// Mowing session validation
const mowingSessionSchema = Joi.object({
propertyId: Joi.number().integer().positive().required(),
lawnSectionIds: Joi.array().items(Joi.number().integer().positive()).min(1).required(),
equipmentId: Joi.number().integer().positive().required(),
cutHeightInches: Joi.number().positive().precision(2).required(),
direction: Joi.string().valid('N_S','E_W','NE_SW','NW_SE','CIRCULAR').required(),
gpsTrack: Joi.object().required(),
averageSpeed: Joi.number().positive().required(),
durationSeconds: Joi.number().integer().positive().required(),
totalDistanceMeters: Joi.number().positive().required(),
areaCoveredSqft: Joi.number().positive().optional(),
notes: Joi.string().allow('').optional()
});
// Validation middleware
const validateRequest = (schema) => {
return (req, res, next) => {
@@ -219,9 +234,10 @@ module.exports = {
userProductSchema,
applicationPlanSchema,
applicationLogSchema,
mowingSessionSchema,
idParamSchema,
// Middleware
validateRequest,
validateParams
};
};