Files
turftracker/backend/src/routes/users.js
2025-08-21 07:06:36 -05:00

225 lines
6.6 KiB
JavaScript

const express = require('express');
const pool = require('../config/database');
const { validateRequest, validateParams } = require('../utils/validation');
const { updateUserSchema, idParamSchema } = require('../utils/validation');
const { requireOwnership } = require('../middleware/auth');
const { AppError } = require('../middleware/errorHandler');
const router = express.Router();
// @route GET /api/users/profile
// @desc Get current user profile
// @access Private
router.get('/profile', async (req, res, next) => {
try {
const userResult = await pool.query(
`SELECT u.id, u.email, u.first_name, u.last_name, u.role, u.created_at, u.updated_at,
COUNT(p.id) as property_count
FROM users u
LEFT JOIN properties p ON u.id = p.user_id
WHERE u.id = $1
GROUP BY u.id`,
[req.user.id]
);
if (userResult.rows.length === 0) {
throw new AppError('User not found', 404);
}
const user = userResult.rows[0];
res.json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role,
propertyCount: parseInt(user.property_count),
createdAt: user.created_at,
updatedAt: user.updated_at
}
}
});
} catch (error) {
next(error);
}
});
// @route PUT /api/users/profile
// @desc Update current user profile
// @access Private
router.put('/profile', validateRequest(updateUserSchema), async (req, res, next) => {
try {
const { firstName, lastName, email } = req.body;
const userId = req.user.id;
// Check if email is already taken by another user
if (email) {
const emailCheck = await pool.query(
'SELECT id FROM users WHERE email = $1 AND id != $2',
[email.toLowerCase(), userId]
);
if (emailCheck.rows.length > 0) {
throw new AppError('Email is already taken by another user', 409);
}
}
// Build dynamic update query
const updates = [];
const values = [];
let paramCount = 1;
if (firstName) {
updates.push(`first_name = $${paramCount}`);
values.push(firstName);
paramCount++;
}
if (lastName) {
updates.push(`last_name = $${paramCount}`);
values.push(lastName);
paramCount++;
}
if (email) {
updates.push(`email = $${paramCount}`);
values.push(email.toLowerCase());
paramCount++;
}
if (updates.length === 0) {
throw new AppError('No valid fields to update', 400);
}
updates.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(userId);
const query = `
UPDATE users
SET ${updates.join(', ')}
WHERE id = $${paramCount}
RETURNING id, email, first_name, last_name, role, updated_at
`;
const result = await pool.query(query, values);
const user = result.rows[0];
res.json({
success: true,
message: 'Profile updated successfully',
data: {
user: {
id: user.id,
email: user.email,
firstName: user.first_name,
lastName: user.last_name,
role: user.role,
updatedAt: user.updated_at
}
}
});
} catch (error) {
next(error);
}
});
// @route DELETE /api/users/account
// @desc Delete current user account
// @access Private
router.delete('/account', async (req, res, next) => {
try {
const userId = req.user.id;
// Check if user has any ongoing applications
const ongoingApps = await pool.query(
`SELECT COUNT(*) as count
FROM application_plans
WHERE user_id = $1 AND status IN ('planned', 'in_progress')`,
[userId]
);
if (parseInt(ongoingApps.rows[0].count) > 0) {
throw new AppError('Cannot delete account with ongoing applications. Please complete or cancel them first.', 400);
}
// Delete user (cascading will handle related records)
await pool.query('DELETE FROM users WHERE id = $1', [userId]);
res.json({
success: true,
message: 'Account deleted successfully'
});
} catch (error) {
next(error);
}
});
// @route GET /api/users/stats
// @desc Get user statistics
// @access Private
router.get('/stats', async (req, res, next) => {
try {
const userId = req.user.id;
const statsQuery = `
SELECT
(SELECT COUNT(*) FROM properties WHERE user_id = $1) as total_properties,
(SELECT COUNT(*) FROM lawn_sections ls
JOIN properties p ON ls.property_id = p.id
WHERE p.user_id = $1) as total_sections,
(SELECT COALESCE(SUM(ls.area), 0) FROM lawn_sections ls
JOIN properties p ON ls.property_id = p.id
WHERE p.user_id = $1) as total_area,
(SELECT COUNT(*) FROM user_equipment WHERE user_id = $1) as total_equipment,
(SELECT COUNT(*) FROM application_plans WHERE user_id = $1) as total_plans,
(SELECT COUNT(*) FROM application_logs WHERE user_id = $1) as total_applications,
(SELECT COUNT(*) FROM application_plans
WHERE user_id = $1 AND status = 'completed') as completed_plans,
(SELECT COUNT(*) FROM application_plans
WHERE user_id = $1 AND planned_date >= CURRENT_DATE) as upcoming_plans
`;
const result = await pool.query(statsQuery, [userId]);
const stats = result.rows[0];
// Get recent activity
const recentActivity = await pool.query(
`SELECT 'application' as type, al.application_date as date,
ls.name as section_name, p.name as property_name
FROM application_logs al
JOIN lawn_sections ls ON al.lawn_section_id = ls.id
JOIN properties p ON ls.property_id = p.id
WHERE al.user_id = $1
ORDER BY al.application_date DESC
LIMIT 5`,
[userId]
);
res.json({
success: true,
data: {
stats: {
totalProperties: parseInt(stats.total_properties),
totalSections: parseInt(stats.total_sections),
totalArea: parseFloat(stats.total_area),
totalEquipment: parseInt(stats.total_equipment),
totalPlans: parseInt(stats.total_plans),
totalApplications: parseInt(stats.total_applications),
completedPlans: parseInt(stats.completed_plans),
upcomingPlans: parseInt(stats.upcoming_plans),
completionRate: stats.total_plans > 0 ?
Math.round((stats.completed_plans / stats.total_plans) * 100) : 0
},
recentActivity: recentActivity.rows
}
});
} catch (error) {
next(error);
}
});
module.exports = router;