Initial Claude Run
This commit is contained in:
410
backend/src/routes/properties.js
Normal file
410
backend/src/routes/properties.js
Normal file
@@ -0,0 +1,410 @@
|
||||
const express = require('express');
|
||||
const pool = require('../config/database');
|
||||
const { validateRequest, validateParams } = require('../utils/validation');
|
||||
const { propertySchema, lawnSectionSchema, idParamSchema } = require('../utils/validation');
|
||||
const { AppError } = require('../middleware/errorHandler');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Helper function to calculate polygon area (in square feet)
|
||||
const calculatePolygonArea = (coordinates) => {
|
||||
if (!coordinates || coordinates.length < 3) return 0;
|
||||
|
||||
// Shoelace formula for polygon area
|
||||
let area = 0;
|
||||
const n = coordinates.length;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
area += coordinates[i][0] * coordinates[j][1];
|
||||
area -= coordinates[j][0] * coordinates[i][1];
|
||||
}
|
||||
|
||||
area = Math.abs(area) / 2;
|
||||
|
||||
// Convert from decimal degrees to square feet (approximate)
|
||||
// This is a rough approximation - in production you'd use proper geodesic calculations
|
||||
const avgLat = coordinates.reduce((sum, coord) => sum + coord[1], 0) / n;
|
||||
const meterToFeet = 3.28084;
|
||||
const degToMeter = 111320 * Math.cos(avgLat * Math.PI / 180);
|
||||
|
||||
return area * Math.pow(degToMeter * meterToFeet, 2);
|
||||
};
|
||||
|
||||
// @route GET /api/properties
|
||||
// @desc Get all properties for current user
|
||||
// @access Private
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`SELECT p.*,
|
||||
COUNT(ls.id) as section_count,
|
||||
COALESCE(SUM(ls.area), 0) as calculated_area
|
||||
FROM properties p
|
||||
LEFT JOIN lawn_sections ls ON p.id = ls.property_id
|
||||
WHERE p.user_id = $1
|
||||
GROUP BY p.id
|
||||
ORDER BY p.created_at DESC`,
|
||||
[req.user.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
properties: result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
address: row.address,
|
||||
latitude: parseFloat(row.latitude),
|
||||
longitude: parseFloat(row.longitude),
|
||||
totalArea: parseFloat(row.total_area),
|
||||
calculatedArea: parseFloat(row.calculated_area),
|
||||
sectionCount: parseInt(row.section_count),
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route GET /api/properties/:id
|
||||
// @desc Get single property with sections
|
||||
// @access Private
|
||||
router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.id;
|
||||
|
||||
// Get property
|
||||
const propertyResult = await pool.query(
|
||||
'SELECT * FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (propertyResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
const property = propertyResult.rows[0];
|
||||
|
||||
// Get lawn sections
|
||||
const sectionsResult = await pool.query(
|
||||
'SELECT * FROM lawn_sections WHERE property_id = $1 ORDER BY name',
|
||||
[propertyId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
property: {
|
||||
id: property.id,
|
||||
name: property.name,
|
||||
address: property.address,
|
||||
latitude: parseFloat(property.latitude),
|
||||
longitude: parseFloat(property.longitude),
|
||||
totalArea: parseFloat(property.total_area),
|
||||
createdAt: property.created_at,
|
||||
updatedAt: property.updated_at,
|
||||
sections: sectionsResult.rows.map(section => ({
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
area: parseFloat(section.area),
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/properties
|
||||
// @desc Create new property
|
||||
// @access Private
|
||||
router.post('/', validateRequest(propertySchema), async (req, res, next) => {
|
||||
try {
|
||||
const { name, address, latitude, longitude, totalArea } = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO properties (user_id, name, address, latitude, longitude, total_area)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[req.user.id, name, address, latitude, longitude, totalArea]
|
||||
);
|
||||
|
||||
const property = result.rows[0];
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Property created successfully',
|
||||
data: {
|
||||
property: {
|
||||
id: property.id,
|
||||
name: property.name,
|
||||
address: property.address,
|
||||
latitude: parseFloat(property.latitude),
|
||||
longitude: parseFloat(property.longitude),
|
||||
totalArea: parseFloat(property.total_area),
|
||||
createdAt: property.created_at,
|
||||
updatedAt: property.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/properties/:id
|
||||
// @desc Update property
|
||||
// @access Private
|
||||
router.put('/:id', validateParams(idParamSchema), validateRequest(propertySchema), async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.id;
|
||||
const { name, address, latitude, longitude, totalArea } = req.body;
|
||||
|
||||
// Check if property exists and belongs to user
|
||||
const checkResult = await pool.query(
|
||||
'SELECT id FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (checkResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE properties
|
||||
SET name = $1, address = $2, latitude = $3, longitude = $4, total_area = $5, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6
|
||||
RETURNING *`,
|
||||
[name, address, latitude, longitude, totalArea, propertyId]
|
||||
);
|
||||
|
||||
const property = result.rows[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Property updated successfully',
|
||||
data: {
|
||||
property: {
|
||||
id: property.id,
|
||||
name: property.name,
|
||||
address: property.address,
|
||||
latitude: parseFloat(property.latitude),
|
||||
longitude: parseFloat(property.longitude),
|
||||
totalArea: parseFloat(property.total_area),
|
||||
createdAt: property.created_at,
|
||||
updatedAt: property.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/properties/:id
|
||||
// @desc Delete property
|
||||
// @access Private
|
||||
router.delete('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.id;
|
||||
|
||||
// Check if property exists and belongs to user
|
||||
const checkResult = await pool.query(
|
||||
'SELECT id FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (checkResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
// Check for active applications
|
||||
const activeApps = await pool.query(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM application_plans ap
|
||||
JOIN lawn_sections ls ON ap.lawn_section_id = ls.id
|
||||
WHERE ls.property_id = $1 AND ap.status IN ('planned', 'in_progress')`,
|
||||
[propertyId]
|
||||
);
|
||||
|
||||
if (parseInt(activeApps.rows[0].count) > 0) {
|
||||
throw new AppError('Cannot delete property with active applications', 400);
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM properties WHERE id = $1', [propertyId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Property deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route POST /api/properties/:id/sections
|
||||
// @desc Create lawn section for property
|
||||
// @access Private
|
||||
router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawnSectionSchema), async (req, res, next) => {
|
||||
try {
|
||||
const propertyId = req.params.id;
|
||||
const { name, area, polygonData, grassType, soilType } = req.body;
|
||||
|
||||
// Check if property exists and belongs to user
|
||||
const propertyResult = await pool.query(
|
||||
'SELECT id FROM properties WHERE id = $1 AND user_id = $2',
|
||||
[propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (propertyResult.rows.length === 0) {
|
||||
throw new AppError('Property not found', 404);
|
||||
}
|
||||
|
||||
// Calculate area from polygon if provided
|
||||
let calculatedArea = area;
|
||||
if (polygonData && polygonData.coordinates && polygonData.coordinates[0]) {
|
||||
calculatedArea = calculatePolygonArea(polygonData.coordinates[0]);
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO lawn_sections (property_id, name, area, polygon_data, grass_type, soil_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *`,
|
||||
[propertyId, name, calculatedArea, JSON.stringify(polygonData), grassType, soilType]
|
||||
);
|
||||
|
||||
const section = result.rows[0];
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Lawn section created successfully',
|
||||
data: {
|
||||
section: {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
area: parseFloat(section.area),
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route PUT /api/properties/:propertyId/sections/:sectionId
|
||||
// @desc Update lawn section
|
||||
// @access Private
|
||||
router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
try {
|
||||
const { propertyId, sectionId } = req.params;
|
||||
const { name, area, polygonData, grassType, soilType } = req.body;
|
||||
|
||||
// Check if section exists and user owns the property
|
||||
const checkResult = await pool.query(
|
||||
`SELECT ls.id
|
||||
FROM lawn_sections ls
|
||||
JOIN properties p ON ls.property_id = p.id
|
||||
WHERE ls.id = $1 AND p.id = $2 AND p.user_id = $3`,
|
||||
[sectionId, propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (checkResult.rows.length === 0) {
|
||||
throw new AppError('Lawn section not found', 404);
|
||||
}
|
||||
|
||||
// Calculate area from polygon if provided
|
||||
let calculatedArea = area;
|
||||
if (polygonData && polygonData.coordinates && polygonData.coordinates[0]) {
|
||||
calculatedArea = calculatePolygonArea(polygonData.coordinates[0]);
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE lawn_sections
|
||||
SET name = $1, area = $2, polygon_data = $3, grass_type = $4, soil_type = $5, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $6
|
||||
RETURNING *`,
|
||||
[name, calculatedArea, JSON.stringify(polygonData), grassType, soilType, sectionId]
|
||||
);
|
||||
|
||||
const section = result.rows[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Lawn section updated successfully',
|
||||
data: {
|
||||
section: {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
area: parseFloat(section.area),
|
||||
polygonData: section.polygon_data,
|
||||
grassType: section.grass_type,
|
||||
soilType: section.soil_type,
|
||||
createdAt: section.created_at,
|
||||
updatedAt: section.updated_at
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// @route DELETE /api/properties/:propertyId/sections/:sectionId
|
||||
// @desc Delete lawn section
|
||||
// @access Private
|
||||
router.delete('/:propertyId/sections/:sectionId', async (req, res, next) => {
|
||||
try {
|
||||
const { propertyId, sectionId } = req.params;
|
||||
|
||||
// Check if section exists and user owns the property
|
||||
const checkResult = await pool.query(
|
||||
`SELECT ls.id
|
||||
FROM lawn_sections ls
|
||||
JOIN properties p ON ls.property_id = p.id
|
||||
WHERE ls.id = $1 AND p.id = $2 AND p.user_id = $3`,
|
||||
[sectionId, propertyId, req.user.id]
|
||||
);
|
||||
|
||||
if (checkResult.rows.length === 0) {
|
||||
throw new AppError('Lawn section not found', 404);
|
||||
}
|
||||
|
||||
// Check for active applications
|
||||
const activeApps = await pool.query(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM application_plans
|
||||
WHERE lawn_section_id = $1 AND status IN ('planned', 'in_progress')`,
|
||||
[sectionId]
|
||||
);
|
||||
|
||||
if (parseInt(activeApps.rows[0].count) > 0) {
|
||||
throw new AppError('Cannot delete section with active applications', 400);
|
||||
}
|
||||
|
||||
await pool.query('DELETE FROM lawn_sections WHERE id = $1', [sectionId]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Lawn section deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user