mowing modal

This commit is contained in:
Jake Kasper
2025-09-02 09:40:42 -05:00
parent ae3275299e
commit d566be09b0
7 changed files with 456 additions and 237 deletions

View File

@@ -2,7 +2,7 @@ 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 { mowingSessionSchema, mowingPlanSchema } = require('../utils/validation');
const router = express.Router();
@@ -127,3 +127,100 @@ router.get('/sessions/:id', async (req, res, next) => {
module.exports = router;
// ----- Plans -----
router.post('/plans', validateRequest(mowingPlanSchema), async (req, res, next) => {
try {
const { propertyId, lawnSectionIds, equipmentId, plannedDate, cutHeightInches, direction, notes } = req.body;
// Ownership checks
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 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_plans (user_id, property_id, equipment_id, planned_date, cut_height_inches, direction, notes)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[req.user.id, propertyId, equipmentId, plannedDate, cutHeightInches, direction, notes || null]
);
const plan = ins.rows[0];
for (const sid of lawnSectionIds) {
await client.query(`INSERT INTO mowing_plan_sections (plan_id, lawn_section_id) VALUES ($1,$2)`, [plan.id, sid]);
}
await client.query('COMMIT');
res.status(201).json({ success: true, data: { plan: { id: plan.id } } });
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release();
}
} catch (error) { next(error); }
});
router.get('/plans', async (req, res, next) => {
try {
const rows = await pool.query(
`SELECT mp.*, p.name as property_name, ue.custom_name as equipment_name,
STRING_AGG(ls.name, ', ') as section_names,
SUM(ls.area) as total_area
FROM mowing_plans mp
JOIN properties p ON mp.property_id=p.id
LEFT JOIN user_equipment ue ON mp.equipment_id=ue.id
JOIN mowing_plan_sections mps ON mp.id=mps.plan_id
JOIN lawn_sections ls ON mps.lawn_section_id=ls.id
WHERE mp.user_id=$1
GROUP BY mp.id, p.name, ue.custom_name
ORDER BY mp.planned_date DESC, mp.created_at DESC
LIMIT 200`,
[req.user.id]
);
res.json({ success: true, data: { plans: rows.rows } });
} catch (error) { next(error); }
});
router.get('/plans/:id', async (req, res, next) => {
try {
const { id } = req.params;
const planRes = await pool.query(
`SELECT mp.*, p.name as property_name, ue.custom_name as equipment_name
FROM mowing_plans mp
JOIN properties p ON mp.property_id=p.id
LEFT JOIN user_equipment ue ON mp.equipment_id=ue.id
WHERE mp.id=$1 AND mp.user_id=$2`, [id, req.user.id]);
if (planRes.rows.length === 0) throw new AppError('Plan not found', 404);
const sectionsRes = await pool.query(
`SELECT ls.id, ls.name, ls.area, ls.polygon_data
FROM mowing_plan_sections mps
JOIN lawn_sections ls ON mps.lawn_section_id=ls.id
WHERE mps.plan_id=$1`, [id]);
res.json({ success: true, data: { plan: planRes.rows[0], sections: sectionsRes.rows } });
} catch (error) { next(error); }
});
router.put('/plans/:id/status', async (req, res, next) => {
try {
const { id } = req.params; const { status } = req.body;
const ok = ['planned','in_progress','completed','archived'];
if (!ok.includes(status)) throw new AppError('Invalid status', 400);
const upd = await pool.query(`UPDATE mowing_plans SET status=$1, updated_at=CURRENT_TIMESTAMP WHERE id=$2 AND user_id=$3`, [status, id, req.user.id]);
if (upd.rowCount === 0) throw new AppError('Plan not found', 404);
res.json({ success: true });
} catch (error) { next(error); }
});
// Alias /logs to sessions to be consistent with Applications
router.get('/logs', async (req, res, next) => {
try {
const rs = 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: { logs: rs.rows } });
} catch (error) { next(error); }
});

View File

@@ -195,6 +195,16 @@ const mowingSessionSchema = Joi.object({
notes: Joi.string().allow('').optional()
});
const mowingPlanSchema = 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(),
plannedDate: Joi.date().required(),
cutHeightInches: Joi.number().positive().precision(2).required(),
direction: Joi.string().valid('N_S','E_W','NE_SW','NW_SE','CIRCULAR').required(),
notes: Joi.string().allow('').optional()
});
// Validation middleware
const validateRequest = (schema) => {
return (req, res, next) => {
@@ -235,6 +245,7 @@ module.exports = {
applicationPlanSchema,
applicationLogSchema,
mowingSessionSchema,
mowingPlanSchema,
idParamSchema,
// Middleware