894 lines
28 KiB
JavaScript
894 lines
28 KiB
JavaScript
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; |