const express = require('express'); const pool = require('../config/database'); const { validateRequest, validateParams } = require('../utils/validation'); const { productSchema, idParamSchema } = require('../utils/validation'); const { requireAdmin } = require('../middleware/auth'); const { AppError } = require('../middleware/errorHandler'); const router = express.Router(); // Apply admin middleware to all routes router.use(requireAdmin); // @route GET /api/admin/dashboard // @desc Get admin dashboard statistics // @access Private (Admin) router.get('/dashboard', async (req, res, next) => { try { const statsQuery = ` SELECT (SELECT COUNT(*) FROM users) as total_users, (SELECT COUNT(*) FROM users WHERE role = 'admin') as admin_users, (SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE - INTERVAL '30 days') as new_users_30d, (SELECT COUNT(*) FROM properties) as total_properties, (SELECT COUNT(*) FROM user_equipment) as total_equipment, (SELECT COUNT(*) FROM products) as total_products, (SELECT COUNT(*) FROM user_products) as custom_products, (SELECT COUNT(*) FROM application_plans) as total_plans, (SELECT COUNT(*) FROM application_logs) as total_applications, (SELECT COUNT(*) FROM application_logs WHERE application_date >= CURRENT_DATE - INTERVAL '7 days') as recent_applications `; const statsResult = await pool.query(statsQuery); const stats = statsResult.rows[0]; // Get user activity (users with recent activity) const userActivityQuery = ` SELECT DATE_TRUNC('day', created_at) as date, COUNT(*) as new_registrations FROM users WHERE created_at >= CURRENT_DATE - INTERVAL '30 days' GROUP BY DATE_TRUNC('day', created_at) ORDER BY date DESC `; const activityResult = await pool.query(userActivityQuery); // Get application activity const applicationActivityQuery = ` SELECT DATE_TRUNC('day', application_date) as date, COUNT(*) as applications, COUNT(DISTINCT user_id) as active_users FROM application_logs WHERE application_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY DATE_TRUNC('day', application_date) ORDER BY date DESC `; const appActivityResult = await pool.query(applicationActivityQuery); res.json({ success: true, data: { stats: { totalUsers: parseInt(stats.total_users), adminUsers: parseInt(stats.admin_users), newUsers30d: parseInt(stats.new_users_30d), totalProperties: parseInt(stats.total_properties), totalEquipment: parseInt(stats.total_equipment), totalProducts: parseInt(stats.total_products), customProducts: parseInt(stats.custom_products), totalPlans: parseInt(stats.total_plans), totalApplications: parseInt(stats.total_applications), recentApplications: parseInt(stats.recent_applications) }, userActivity: activityResult.rows.map(row => ({ date: row.date, newRegistrations: parseInt(row.new_registrations) })), applicationActivity: appActivityResult.rows.map(row => ({ date: row.date, applications: parseInt(row.applications), activeUsers: parseInt(row.active_users) })) } }); } catch (error) { next(error); } }); // @route GET /api/admin/users // @desc Get all users with pagination // @access Private (Admin) router.get('/users', async (req, res, next) => { try { const { page = 1, limit = 20, search, role } = req.query; const offset = (page - 1) * limit; let whereConditions = []; let queryParams = []; let paramCount = 0; if (search) { paramCount++; whereConditions.push(`(first_name ILIKE $${paramCount} OR last_name ILIKE $${paramCount} OR email ILIKE $${paramCount})`); queryParams.push(`%${search}%`); } if (role) { paramCount++; whereConditions.push(`role = $${paramCount}`); queryParams.push(role); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; // Get total count const countQuery = `SELECT COUNT(*) as total FROM users ${whereClause}`; const countResult = await pool.query(countQuery, queryParams); const totalUsers = parseInt(countResult.rows[0].total); // Get users with stats paramCount++; queryParams.push(limit); paramCount++; queryParams.push(offset); const usersQuery = ` SELECT u.id, u.email, u.first_name, u.last_name, u.role, u.oauth_provider, u.created_at, u.updated_at, COUNT(DISTINCT p.id) as property_count, COUNT(DISTINCT ap.id) as application_count, MAX(al.application_date) as last_application FROM users u LEFT JOIN properties p ON u.id = p.user_id LEFT JOIN application_plans ap ON u.id = ap.user_id LEFT JOIN application_logs al ON u.id = al.user_id ${whereClause} GROUP BY u.id ORDER BY u.created_at DESC LIMIT $${paramCount - 1} OFFSET $${paramCount} `; const usersResult = await pool.query(usersQuery, queryParams); res.json({ success: true, data: { users: usersResult.rows.map(user => ({ id: user.id, email: user.email, firstName: user.first_name, lastName: user.last_name, role: user.role, oauthProvider: user.oauth_provider, propertyCount: parseInt(user.property_count), applicationCount: parseInt(user.application_count), lastApplication: user.last_application, createdAt: user.created_at, updatedAt: user.updated_at })), pagination: { currentPage: parseInt(page), totalPages: Math.ceil(totalUsers / limit), totalUsers, hasNext: (page * limit) < totalUsers, hasPrev: page > 1 } } }); } catch (error) { next(error); } }); // @route PUT /api/admin/users/:id/role // @desc Update user role // @access Private (Admin) router.put('/users/:id/role', validateParams(idParamSchema), async (req, res, next) => { try { const userId = req.params.id; const { role } = req.body; if (!['admin', 'user'].includes(role)) { throw new AppError('Invalid role', 400); } // Prevent removing admin role from yourself if (parseInt(userId) === req.user.id && role !== 'admin') { throw new AppError('Cannot remove admin role from yourself', 400); } // Check if user exists const userCheck = await pool.query( 'SELECT id, role FROM users WHERE id = $1', [userId] ); if (userCheck.rows.length === 0) { throw new AppError('User not found', 404); } // Update role const result = await pool.query( 'UPDATE users SET role = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING id, email, role', [role, userId] ); const user = result.rows[0]; res.json({ success: true, message: 'User role updated successfully', data: { user: { id: user.id, email: user.email, role: user.role } } }); } catch (error) { next(error); } }); // @route DELETE /api/admin/users/:id // @desc Delete user account // @access Private (Admin) router.delete('/users/:id', validateParams(idParamSchema), async (req, res, next) => { try { const userId = req.params.id; // Prevent deleting yourself if (parseInt(userId) === req.user.id) { throw new AppError('Cannot delete your own account', 400); } // Check if user exists const userCheck = await pool.query( 'SELECT id, email FROM users WHERE id = $1', [userId] ); if (userCheck.rows.length === 0) { throw new AppError('User not found', 404); } const user = userCheck.rows[0]; // Delete user (cascading will handle related records) await pool.query('DELETE FROM users WHERE id = $1', [userId]); res.json({ success: true, message: `User ${user.email} deleted successfully` }); } catch (error) { next(error); } }); // @route GET /api/admin/products // @desc Get all products for management // @access Private (Admin) router.get('/products', async (req, res, next) => { try { const { category, search } = req.query; let whereConditions = []; let queryParams = []; let paramCount = 0; if (category) { paramCount++; whereConditions.push(`p.category_id = $${paramCount}`); queryParams.push(category); } if (search) { paramCount++; whereConditions.push(`(p.name ILIKE $${paramCount} OR p.brand ILIKE $${paramCount})`); queryParams.push(`%${search}%`); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; const result = await pool.query( `SELECT p.*, pc.name as category_name, COUNT(pr.id) as rate_count, COUNT(up.id) as usage_count FROM products p JOIN product_categories pc ON p.category_id = pc.id LEFT JOIN product_rates pr ON p.id = pr.product_id LEFT JOIN user_products up ON p.id = up.product_id ${whereClause} GROUP BY p.id, pc.name ORDER BY p.name`, queryParams ); res.json({ success: true, data: { products: result.rows.map(product => ({ id: product.id, name: product.name, brand: product.brand, categoryName: product.category_name, productType: product.product_type, activeIngredients: product.active_ingredients, description: product.description, rateCount: parseInt(product.rate_count), usageCount: parseInt(product.usage_count), createdAt: product.created_at })) } }); } catch (error) { next(error); } }); // @route POST /api/admin/products // @desc Create new product // @access Private (Admin) router.post('/products', validateRequest(productSchema), async (req, res, next) => { try { const { name, brand, categoryId, productType, activeIngredients, description } = req.body; // Check if category exists const categoryCheck = await pool.query( 'SELECT id FROM product_categories WHERE id = $1', [categoryId] ); if (categoryCheck.rows.length === 0) { throw new AppError('Product category not found', 404); } const result = await pool.query( `INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [name, brand, categoryId, productType, activeIngredients, description] ); const product = result.rows[0]; res.status(201).json({ success: true, message: 'Product created successfully', data: { product: { id: product.id, name: product.name, brand: product.brand, categoryId: product.category_id, productType: product.product_type, activeIngredients: product.active_ingredients, description: product.description, createdAt: product.created_at } } }); } catch (error) { next(error); } }); // @route PUT /api/admin/products/:id // @desc Update product // @access Private (Admin) router.put('/products/:id', validateParams(idParamSchema), validateRequest(productSchema), async (req, res, next) => { try { const productId = req.params.id; const { name, brand, categoryId, productType, activeIngredients, description } = req.body; // Check if product exists const productCheck = await pool.query( 'SELECT id FROM products WHERE id = $1', [productId] ); if (productCheck.rows.length === 0) { throw new AppError('Product not found', 404); } // Check if category exists const categoryCheck = await pool.query( 'SELECT id FROM product_categories WHERE id = $1', [categoryId] ); if (categoryCheck.rows.length === 0) { throw new AppError('Product category not found', 404); } const result = await pool.query( `UPDATE products SET name = $1, brand = $2, category_id = $3, product_type = $4, active_ingredients = $5, description = $6 WHERE id = $7 RETURNING *`, [name, brand, categoryId, productType, activeIngredients, description, productId] ); const product = result.rows[0]; res.json({ success: true, message: 'Product updated successfully', data: { product: { id: product.id, name: product.name, brand: product.brand, categoryId: product.category_id, productType: product.product_type, activeIngredients: product.active_ingredients, description: product.description, createdAt: product.created_at } } }); } catch (error) { next(error); } }); // @route DELETE /api/admin/products/:id // @desc Delete product // @access Private (Admin) router.delete('/products/:id', validateParams(idParamSchema), async (req, res, next) => { try { const productId = req.params.id; // Check if product exists const productCheck = await pool.query( 'SELECT id, name FROM products WHERE id = $1', [productId] ); if (productCheck.rows.length === 0) { throw new AppError('Product not found', 404); } const product = productCheck.rows[0]; // Check if product is used in any user products or applications const usageCheck = await pool.query( `SELECT (SELECT COUNT(*) FROM user_products WHERE product_id = $1) + (SELECT COUNT(*) FROM application_plan_products WHERE product_id = $1) + (SELECT COUNT(*) FROM application_log_products WHERE product_id = $1) as usage_count`, [productId] ); if (parseInt(usageCheck.rows[0].usage_count) > 0) { throw new AppError('Cannot delete product that is being used by users', 400); } await pool.query('DELETE FROM products WHERE id = $1', [productId]); res.json({ success: true, message: `Product "${product.name}" deleted successfully` }); } catch (error) { next(error); } }); // @route GET /api/admin/system/health // @desc Get system health information // @access Private (Admin) router.get('/system/health', async (req, res, next) => { try { // Database connection test const dbResult = await pool.query('SELECT NOW() as timestamp, version() as version'); // Get database statistics const dbStats = await pool.query(` SELECT pg_size_pretty(pg_database_size(current_database())) as database_size, (SELECT count(*) FROM pg_stat_activity WHERE state = 'active') as active_connections, (SELECT setting FROM pg_settings WHERE name = 'max_connections') as max_connections `); const stats = dbStats.rows[0]; res.json({ success: true, data: { system: { status: 'healthy', timestamp: new Date(), uptime: process.uptime(), nodeVersion: process.version, environment: process.env.NODE_ENV || 'development' }, database: { status: 'connected', version: dbResult.rows[0].version, size: stats.database_size, activeConnections: parseInt(stats.active_connections), maxConnections: parseInt(stats.max_connections), timestamp: dbResult.rows[0].timestamp }, services: { weatherApi: { configured: !!process.env.WEATHER_API_KEY, status: process.env.WEATHER_API_KEY ? 'available' : 'not configured' }, googleOAuth: { configured: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET), status: (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) ? 'available' : 'not configured' } } } }); } catch (error) { next(error); } }); // @route GET /api/admin/products/user // @desc Get all user products for admin management // @access Private (Admin) router.get('/products/user', async (req, res, next) => { try { const { search, category, user_id } = req.query; let whereConditions = []; let queryParams = []; let paramCount = 0; if (search) { paramCount++; whereConditions.push(`(up.custom_name ILIKE $${paramCount} OR up.custom_brand ILIKE $${paramCount} OR u.email ILIKE $${paramCount})`); queryParams.push(`%${search}%`); } if (category) { paramCount++; whereConditions.push(`up.category_id = $${paramCount}`); queryParams.push(category); } if (user_id) { paramCount++; whereConditions.push(`up.user_id = $${paramCount}`); queryParams.push(user_id); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; const result = await pool.query(` SELECT up.*, u.email as user_email, u.first_name, u.last_name, pc.name as category_name, p.name as base_product_name, p.brand as base_product_brand, COUNT(pss.id) as spreader_settings_count, COUNT(DISTINCT app.id) as usage_count FROM user_products up JOIN users u ON up.user_id = u.id LEFT JOIN product_categories pc ON up.category_id = pc.id LEFT JOIN products p ON up.product_id = p.id LEFT JOIN product_spreader_settings pss ON up.id = pss.user_product_id LEFT JOIN application_plan_products app ON up.id = app.user_product_id ${whereClause} GROUP BY up.id, u.email, u.first_name, u.last_name, pc.name, p.name, p.brand ORDER BY u.email, up.custom_name `, queryParams); res.json({ success: true, data: { userProducts: result.rows.map(product => ({ id: product.id, userId: product.user_id, userEmail: product.user_email, userName: `${product.first_name} ${product.last_name}`, baseProductId: product.product_id, baseProductName: product.base_product_name, baseProductBrand: product.base_product_brand, customName: product.custom_name, customBrand: product.custom_brand, categoryId: product.category_id, categoryName: product.category_name, productType: product.custom_product_type, activeIngredients: product.custom_active_ingredients, description: product.custom_description, rateAmount: product.custom_rate_amount, rateUnit: product.custom_rate_unit, notes: product.notes, spreaderSettingsCount: parseInt(product.spreader_settings_count), usageCount: parseInt(product.usage_count), createdAt: product.created_at, updatedAt: product.updated_at })) } }); } catch (error) { next(error); } }); // @route POST /api/admin/products/user/:id/promote // @desc Promote user product to shared product // @access Private (Admin) router.post('/products/user/:id/promote', validateParams(idParamSchema), async (req, res, next) => { try { const userProductId = req.params.id; // Get the user product const userProductResult = await pool.query( `SELECT up.*, pc.name as category_name FROM user_products up LEFT JOIN product_categories pc ON up.category_id = pc.id WHERE up.id = $1`, [userProductId] ); if (userProductResult.rows.length === 0) { throw new AppError('User product not found', 404); } const userProduct = userProductResult.rows[0]; // Create new shared product const newProductResult = await pool.query(` INSERT INTO products (name, brand, category_id, product_type, active_ingredients, description) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `, [ userProduct.custom_name, userProduct.custom_brand || 'Generic', userProduct.category_id, userProduct.custom_product_type, userProduct.custom_active_ingredients, userProduct.custom_description ]); const newProduct = newProductResult.rows[0]; // Create default rate if custom rate exists if (userProduct.custom_rate_amount && userProduct.custom_rate_unit) { await pool.query(` INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes) VALUES ($1, $2, $3, $4, $5) `, [ newProduct.id, userProduct.custom_product_type, userProduct.custom_rate_amount, userProduct.custom_rate_unit, `Promoted from user product: ${userProduct.notes || ''}` ]); } res.json({ success: true, message: `User product "${userProduct.custom_name}" promoted to shared product`, data: { newProduct: { id: newProduct.id, name: newProduct.name, brand: newProduct.brand, categoryId: newProduct.category_id, productType: newProduct.product_type, activeIngredients: newProduct.active_ingredients, description: newProduct.description } } }); } catch (error) { next(error); } }); // @route GET /api/admin/equipment/user // @desc Get all user equipment for admin management // @access Private (Admin) router.get('/equipment/user', async (req, res, next) => { try { const { search, category, user_id } = req.query; let whereConditions = []; let queryParams = []; let paramCount = 0; if (search) { paramCount++; whereConditions.push(`(ue.custom_name ILIKE $${paramCount} OR ue.manufacturer ILIKE $${paramCount} OR u.email ILIKE $${paramCount})`); queryParams.push(`%${search}%`); } if (category) { paramCount++; whereConditions.push(`ue.category_id = $${paramCount}`); queryParams.push(category); } if (user_id) { paramCount++; whereConditions.push(`ue.user_id = $${paramCount}`); queryParams.push(user_id); } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; const result = await pool.query(` SELECT ue.*, u.email as user_email, u.first_name, u.last_name, ec.name as category_name, et.name as type_name, et.manufacturer as type_manufacturer, et.model as type_model FROM user_equipment ue JOIN users u ON ue.user_id = u.id LEFT JOIN equipment_categories ec ON ue.category_id = ec.id LEFT JOIN equipment_types et ON ue.equipment_type_id = et.id ${whereClause} ORDER BY u.email, ue.custom_name `, queryParams); res.json({ success: true, data: { userEquipment: result.rows.map(equipment => ({ id: equipment.id, userId: equipment.user_id, userEmail: equipment.user_email, userName: `${equipment.first_name} ${equipment.last_name}`, equipmentTypeId: equipment.equipment_type_id, categoryId: equipment.category_id, typeName: equipment.type_name, typeManufacturer: equipment.type_manufacturer, typeModel: equipment.type_model, categoryName: equipment.category_name, customName: equipment.custom_name, manufacturer: equipment.manufacturer, model: equipment.model, // Spreader fields capacityLbs: parseFloat(equipment.capacity_lbs) || null, spreaderType: equipment.spreader_type, spreadWidth: parseFloat(equipment.spread_width) || null, // Sprayer fields tankCapacityGallons: parseFloat(equipment.tank_capacity_gallons) || null, sprayerType: equipment.sprayer_type, boomWidth: parseFloat(equipment.boom_width) || null, numberOfNozzles: parseInt(equipment.number_of_nozzles) || null, // Common fields yearManufactured: parseInt(equipment.year_manufactured) || null, serialNumber: equipment.serial_number, notes: equipment.notes, isActive: equipment.is_active, createdAt: equipment.created_at, updatedAt: equipment.updated_at })) } }); } catch (error) { next(error); } }); // @route POST /api/admin/equipment/user/:id/promote // @desc Promote user equipment to shared equipment type // @access Private (Admin) router.post('/equipment/user/:id/promote', validateParams(idParamSchema), async (req, res, next) => { try { const userEquipmentId = req.params.id; // Get the user equipment const userEquipmentResult = await pool.query( `SELECT ue.*, ec.name as category_name FROM user_equipment ue LEFT JOIN equipment_categories ec ON ue.category_id = ec.id WHERE ue.id = $1`, [userEquipmentId] ); if (userEquipmentResult.rows.length === 0) { throw new AppError('User equipment not found', 404); } const userEquipment = userEquipmentResult.rows[0]; // Create new shared equipment type const newEquipmentTypeResult = await pool.query(` INSERT INTO equipment_types (name, manufacturer, model, category_id) VALUES ($1, $2, $3, $4) RETURNING * `, [ userEquipment.custom_name, userEquipment.manufacturer || 'Generic', userEquipment.model || 'Standard', userEquipment.category_id ]); const newEquipmentType = newEquipmentTypeResult.rows[0]; res.json({ success: true, message: `User equipment "${userEquipment.custom_name}" promoted to shared equipment type`, data: { newEquipmentType: { id: newEquipmentType.id, name: newEquipmentType.name, manufacturer: newEquipmentType.manufacturer, model: newEquipmentType.model, categoryId: newEquipmentType.category_id } } }); } catch (error) { next(error); } }); // @route GET /api/admin/products/:id/rates // @desc Get application rates for a specific shared product // @access Private (Admin) router.get('/products/:id/rates', validateParams(idParamSchema), async (req, res, next) => { try { const productId = req.params.id; const result = await pool.query(` SELECT pr.*, p.name as product_name, p.brand as product_brand FROM product_rates pr JOIN products p ON pr.product_id = p.id WHERE pr.product_id = $1 ORDER BY pr.application_type, pr.rate_amount `, [productId]); res.json({ success: true, data: { rates: result.rows.map(rate => ({ id: rate.id, productId: rate.product_id, productName: rate.product_name, productBrand: rate.product_brand, applicationType: rate.application_type, rateAmount: parseFloat(rate.rate_amount), rateUnit: rate.rate_unit, notes: rate.notes, createdAt: rate.created_at })) } }); } catch (error) { next(error); } }); // @route GET /api/admin/products/user/:id/spreader-settings // @desc Get spreader settings for a specific user product // @access Private (Admin) router.get('/products/user/:id/spreader-settings', validateParams(idParamSchema), async (req, res, next) => { try { const userProductId = req.params.id; const result = await pool.query(` SELECT pss.*, ue.custom_name as equipment_name, ss.brand_name, ss.model, ss.setting_number FROM product_spreader_settings pss JOIN user_equipment ue ON pss.user_equipment_id = ue.id LEFT JOIN spreader_settings ss ON pss.spreader_setting_id = ss.id WHERE pss.user_product_id = $1 ORDER BY ue.custom_name, ss.brand_name `, [userProductId]); res.json({ success: true, data: { spreaderSettings: result.rows.map(setting => ({ id: setting.id, userProductId: setting.user_product_id, userEquipmentId: setting.user_equipment_id, equipmentName: setting.equipment_name, spreaderSettingId: setting.spreader_setting_id, brandName: setting.brand_name, model: setting.model, settingNumber: setting.setting_number, customRate: setting.custom_rate ? parseFloat(setting.custom_rate) : null, customRateUnit: setting.custom_rate_unit, notes: setting.notes, createdAt: setting.created_at })) } }); } catch (error) { next(error); } }); module.exports = router;