724 lines
23 KiB
JavaScript
724 lines
23 KiB
JavaScript
const express = require('express');
|
|
const pool = require('../config/database');
|
|
const { validateRequest, validateParams } = require('../utils/validation');
|
|
const { productSchema, productRateSchema, userProductSchema, idParamSchema } = require('../utils/validation');
|
|
const { AppError } = require('../middleware/errorHandler');
|
|
|
|
const router = express.Router();
|
|
|
|
// @route GET /api/products/categories
|
|
// @desc Get all product categories
|
|
// @access Private
|
|
router.get('/categories', async (req, res, next) => {
|
|
try {
|
|
const result = await pool.query(
|
|
'SELECT * FROM product_categories ORDER BY name'
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
categories: result.rows
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route GET /api/products
|
|
// @desc Get all products (shared + user's custom products)
|
|
// @access Private
|
|
router.get('/', async (req, res, next) => {
|
|
try {
|
|
const { category, type, search } = req.query;
|
|
|
|
let whereConditions = [];
|
|
let queryParams = [req.user.id];
|
|
let paramCount = 1;
|
|
|
|
// Build WHERE clause for filtering
|
|
if (category) {
|
|
paramCount++;
|
|
whereConditions.push(`pc.id = $${paramCount}`);
|
|
queryParams.push(category);
|
|
}
|
|
|
|
if (type) {
|
|
paramCount++;
|
|
whereConditions.push(`p.product_type = $${paramCount}`);
|
|
queryParams.push(type);
|
|
}
|
|
|
|
if (search) {
|
|
paramCount++;
|
|
const searchParam1 = paramCount;
|
|
paramCount++;
|
|
const searchParam2 = paramCount;
|
|
paramCount++;
|
|
const searchParam3 = paramCount;
|
|
whereConditions.push(`(p.name ILIKE $${searchParam1} OR p.brand ILIKE $${searchParam2} OR p.active_ingredients ILIKE $${searchParam3})`);
|
|
queryParams.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
|
}
|
|
|
|
const whereClause = whereConditions.length > 0 ? `AND ${whereConditions.join(' AND ')}` : '';
|
|
|
|
// Get shared products
|
|
const sharedProductsQuery = `
|
|
SELECT p.*, pc.name as category_name,
|
|
array_agg(
|
|
json_build_object(
|
|
'id', pr.id,
|
|
'applicationType', pr.application_type,
|
|
'rateAmount', pr.rate_amount,
|
|
'rateUnit', pr.rate_unit,
|
|
'notes', pr.notes
|
|
)
|
|
) FILTER (WHERE pr.id IS NOT NULL) as rates
|
|
FROM products p
|
|
JOIN product_categories pc ON p.category_id = pc.id
|
|
LEFT JOIN product_rates pr ON p.id = pr.product_id
|
|
WHERE 1=1 ${whereClause}
|
|
GROUP BY p.id, pc.name
|
|
ORDER BY p.name
|
|
`;
|
|
|
|
const sharedResult = await pool.query(sharedProductsQuery, queryParams.slice(1));
|
|
|
|
// Get user's custom products
|
|
const userProductsQuery = `
|
|
SELECT up.*, p.name as base_product_name, p.brand as base_brand, p.product_type as base_product_type,
|
|
pc.name as category_name, upc.name as custom_category_name
|
|
FROM user_products up
|
|
LEFT JOIN products p ON up.product_id = p.id
|
|
LEFT JOIN product_categories pc ON p.category_id = pc.id
|
|
LEFT JOIN product_categories upc ON up.category_id = upc.id
|
|
WHERE up.user_id = $1
|
|
ORDER BY COALESCE(up.custom_name, p.name)
|
|
`;
|
|
|
|
const userResult = await pool.query(userProductsQuery, [req.user.id]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
sharedProducts: sharedResult.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,
|
|
rates: product.rates || [],
|
|
isShared: true,
|
|
createdAt: product.created_at
|
|
})),
|
|
userProducts: userResult.rows.map(product => ({
|
|
id: product.id,
|
|
baseProductId: product.product_id,
|
|
baseProductName: product.base_product_name,
|
|
customName: product.custom_name,
|
|
// Use custom fields if available, otherwise fall back to base product
|
|
brand: product.custom_brand || product.base_brand,
|
|
customBrand: product.custom_brand,
|
|
categoryId: product.category_id,
|
|
categoryName: product.custom_category_name || product.category_name,
|
|
productType: product.custom_product_type || product.base_product_type || null,
|
|
customProductType: product.custom_product_type,
|
|
activeIngredients: product.custom_active_ingredients,
|
|
customActiveIngredients: product.custom_active_ingredients,
|
|
description: product.custom_description,
|
|
customDescription: product.custom_description,
|
|
customRateAmount: parseFloat(product.custom_rate_amount),
|
|
customRateUnit: product.custom_rate_unit,
|
|
notes: product.notes,
|
|
isShared: false,
|
|
createdAt: product.created_at,
|
|
updatedAt: product.updated_at
|
|
}))
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route GET /api/products/:id
|
|
// @desc Get single shared product with rates
|
|
// @access Private
|
|
router.get('/:id', validateParams(idParamSchema), async (req, res, next) => {
|
|
try {
|
|
const productId = req.params.id;
|
|
|
|
const productResult = await pool.query(
|
|
`SELECT p.*, pc.name as category_name
|
|
FROM products p
|
|
JOIN product_categories pc ON p.category_id = pc.id
|
|
WHERE p.id = $1`,
|
|
[productId]
|
|
);
|
|
|
|
if (productResult.rows.length === 0) {
|
|
throw new AppError('Product not found', 404);
|
|
}
|
|
|
|
const product = productResult.rows[0];
|
|
|
|
// Get product rates
|
|
const ratesResult = await pool.query(
|
|
'SELECT * FROM product_rates WHERE product_id = $1 ORDER BY application_type',
|
|
[productId]
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
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,
|
|
rates: ratesResult.rows.map(rate => ({
|
|
id: rate.id,
|
|
applicationType: rate.application_type,
|
|
rateAmount: parseFloat(rate.rate_amount),
|
|
rateUnit: rate.rate_unit,
|
|
notes: rate.notes,
|
|
createdAt: rate.created_at
|
|
})),
|
|
createdAt: product.created_at
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route POST /api/products
|
|
// @desc Create new shared product (admin only)
|
|
// @access Private (Admin)
|
|
router.post('/', validateRequest(productSchema), async (req, res, next) => {
|
|
try {
|
|
// Check if user is admin
|
|
if (req.user.role !== 'admin') {
|
|
throw new AppError('Admin access required', 403);
|
|
}
|
|
|
|
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 POST /api/products/:id/rates
|
|
// @desc Add application rate to product (admin only)
|
|
// @access Private (Admin)
|
|
router.post('/:id/rates', validateParams(idParamSchema), validateRequest(productRateSchema), async (req, res, next) => {
|
|
try {
|
|
// Check if user is admin
|
|
if (req.user.role !== 'admin') {
|
|
throw new AppError('Admin access required', 403);
|
|
}
|
|
|
|
const productId = req.params.id;
|
|
const { applicationType, rateAmount, rateUnit, notes } = 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);
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO product_rates (product_id, application_type, rate_amount, rate_unit, notes)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *`,
|
|
[productId, applicationType, rateAmount, rateUnit, notes]
|
|
);
|
|
|
|
const rate = result.rows[0];
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Application rate added successfully',
|
|
data: {
|
|
rate: {
|
|
id: rate.id,
|
|
productId: rate.product_id,
|
|
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 POST /api/products/user
|
|
// @desc Create user's custom product
|
|
// @access Private
|
|
router.post('/user', validateRequest(userProductSchema), async (req, res, next) => {
|
|
try {
|
|
const {
|
|
productId,
|
|
customName,
|
|
customRateAmount,
|
|
customRateUnit,
|
|
notes,
|
|
// Advanced fields for creating custom products
|
|
brand,
|
|
categoryId,
|
|
productType,
|
|
activeIngredients,
|
|
description,
|
|
spreaderSettings
|
|
} = req.body;
|
|
|
|
// If based on existing product, verify it exists
|
|
if (productId) {
|
|
const productCheck = await pool.query(
|
|
'SELECT id FROM products WHERE id = $1',
|
|
[productId]
|
|
);
|
|
|
|
if (productCheck.rows.length === 0) {
|
|
throw new AppError('Base product not found', 404);
|
|
}
|
|
}
|
|
|
|
// Require either productId or customName
|
|
if (!productId && !customName) {
|
|
throw new AppError('Either base product or custom name is required', 400);
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO user_products (user_id, product_id, custom_name, custom_brand, category_id,
|
|
custom_product_type, custom_active_ingredients, custom_description,
|
|
custom_rate_amount, custom_rate_unit, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
RETURNING *`,
|
|
[req.user.id, productId, customName, brand, categoryId, productType, activeIngredients,
|
|
description, customRateAmount, customRateUnit, notes]
|
|
);
|
|
|
|
const userProduct = result.rows[0];
|
|
|
|
// Handle spreader settings for granular products
|
|
if (spreaderSettings && Array.isArray(spreaderSettings) && productType === 'granular') {
|
|
// Add spreader settings for this user product
|
|
for (const setting of spreaderSettings) {
|
|
// Check if equipment exists and belongs to user if equipmentId is provided
|
|
if (setting.equipmentId) {
|
|
const equipmentCheck = await pool.query(
|
|
'SELECT id FROM user_equipment WHERE id = $1 AND user_id = $2',
|
|
[setting.equipmentId, req.user.id]
|
|
);
|
|
|
|
if (equipmentCheck.rows.length === 0) {
|
|
throw new AppError(`Equipment with id ${setting.equipmentId} not found`, 404);
|
|
}
|
|
|
|
await pool.query(
|
|
`INSERT INTO product_spreader_settings
|
|
(user_product_id, equipment_id, setting_value, rate_description, notes)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
[userProduct.id, setting.equipmentId, setting.settingValue,
|
|
setting.rateDescription, setting.notes]
|
|
);
|
|
} else {
|
|
// Fall back to legacy brand/model approach
|
|
await pool.query(
|
|
`INSERT INTO product_spreader_settings
|
|
(user_product_id, spreader_brand, spreader_model, setting_value, rate_description, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
[userProduct.id, setting.spreaderBrand, setting.spreaderModel, setting.settingValue,
|
|
setting.rateDescription, setting.notes]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: 'Custom product created successfully',
|
|
data: {
|
|
userProduct: {
|
|
id: userProduct.id,
|
|
baseProductId: userProduct.product_id,
|
|
customName: userProduct.custom_name,
|
|
customBrand: userProduct.custom_brand,
|
|
categoryId: userProduct.category_id,
|
|
customProductType: userProduct.custom_product_type,
|
|
customActiveIngredients: userProduct.custom_active_ingredients,
|
|
customDescription: userProduct.custom_description,
|
|
customRateAmount: parseFloat(userProduct.custom_rate_amount),
|
|
customRateUnit: userProduct.custom_rate_unit,
|
|
notes: userProduct.notes,
|
|
createdAt: userProduct.created_at,
|
|
updatedAt: userProduct.updated_at
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route GET /api/products/user/:id
|
|
// @desc Get user's custom product
|
|
// @access Private
|
|
router.get('/user/:id', validateParams(idParamSchema), async (req, res, next) => {
|
|
try {
|
|
const userProductId = req.params.id;
|
|
|
|
const result = await pool.query(
|
|
`SELECT up.*, p.name as base_product_name, p.brand as base_brand, p.product_type as base_product_type,
|
|
p.active_ingredients as base_active_ingredients, pc.name as base_category_name,
|
|
upc.name as custom_category_name
|
|
FROM user_products up
|
|
LEFT JOIN products p ON up.product_id = p.id
|
|
LEFT JOIN product_categories pc ON p.category_id = pc.id
|
|
LEFT JOIN product_categories upc ON up.category_id = upc.id
|
|
WHERE up.id = $1 AND up.user_id = $2`,
|
|
[userProductId, req.user.id]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
throw new AppError('User product not found', 404);
|
|
}
|
|
|
|
const userProduct = result.rows[0];
|
|
|
|
// Get spreader settings for this user product with equipment details
|
|
let spreaderSettings = [];
|
|
const settingsResult = await pool.query(
|
|
`SELECT pss.*, ue.custom_name as equipment_name, ue.manufacturer, ue.model as equipment_model,
|
|
ue.spreader_type, ue.capacity_lbs
|
|
FROM product_spreader_settings pss
|
|
LEFT JOIN user_equipment ue ON pss.equipment_id = ue.id
|
|
WHERE pss.user_product_id = $1
|
|
ORDER BY ue.custom_name NULLS LAST, pss.spreader_brand, pss.spreader_model NULLS LAST, pss.setting_value`,
|
|
[userProductId]
|
|
);
|
|
|
|
spreaderSettings = settingsResult.rows.map(row => ({
|
|
id: row.id,
|
|
equipmentId: row.equipment_id,
|
|
equipmentName: row.equipment_name,
|
|
equipmentManufacturer: row.manufacturer,
|
|
equipmentModel: row.equipment_model,
|
|
equipmentType: row.spreader_type,
|
|
equipmentCapacity: row.capacity_lbs ? parseFloat(row.capacity_lbs) : null,
|
|
// Legacy fields
|
|
spreaderBrand: row.spreader_brand,
|
|
spreaderModel: row.spreader_model,
|
|
settingValue: row.setting_value,
|
|
rateDescription: row.rate_description,
|
|
notes: row.notes,
|
|
createdAt: row.created_at
|
|
}));
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
userProduct: {
|
|
id: userProduct.id,
|
|
baseProductId: userProduct.product_id,
|
|
baseProductName: userProduct.base_product_name,
|
|
customName: userProduct.custom_name,
|
|
brand: userProduct.custom_brand || userProduct.base_brand,
|
|
categoryName: userProduct.custom_category_name || userProduct.base_category_name,
|
|
categoryId: userProduct.category_id || (userProduct.product_id ? userProduct.category_id : null),
|
|
productType: userProduct.custom_product_type || userProduct.base_product_type,
|
|
activeIngredients: userProduct.custom_active_ingredients || userProduct.base_active_ingredients,
|
|
description: userProduct.custom_description,
|
|
customRateAmount: userProduct.custom_rate_amount ? parseFloat(userProduct.custom_rate_amount) : null,
|
|
customRateUnit: userProduct.custom_rate_unit,
|
|
notes: userProduct.notes,
|
|
spreaderSettings: spreaderSettings,
|
|
createdAt: userProduct.created_at,
|
|
updatedAt: userProduct.updated_at
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route PUT /api/products/user/:id
|
|
// @desc Update user's custom product
|
|
// @access Private
|
|
router.put('/user/:id', validateParams(idParamSchema), validateRequest(userProductSchema), async (req, res, next) => {
|
|
try {
|
|
const userProductId = req.params.id;
|
|
const {
|
|
productId,
|
|
customName,
|
|
customRateAmount,
|
|
customRateUnit,
|
|
notes,
|
|
isAdvancedEdit,
|
|
// Advanced edit fields
|
|
brand,
|
|
categoryId,
|
|
productType,
|
|
activeIngredients,
|
|
description,
|
|
spreaderSettings
|
|
} = req.body;
|
|
|
|
// Check if user product exists and belongs to user
|
|
const checkResult = await pool.query(
|
|
'SELECT id FROM user_products WHERE id = $1 AND user_id = $2',
|
|
[userProductId, req.user.id]
|
|
);
|
|
|
|
if (checkResult.rows.length === 0) {
|
|
throw new AppError('User product not found', 404);
|
|
}
|
|
|
|
// If changing base product, verify it exists
|
|
if (productId) {
|
|
const productCheck = await pool.query(
|
|
'SELECT id FROM products WHERE id = $1',
|
|
[productId]
|
|
);
|
|
|
|
if (productCheck.rows.length === 0) {
|
|
throw new AppError('Base product not found', 404);
|
|
}
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`UPDATE user_products
|
|
SET product_id = $1, custom_name = $2, custom_brand = $3, category_id = $4,
|
|
custom_product_type = $5, custom_active_ingredients = $6, custom_description = $7,
|
|
custom_rate_amount = $8, custom_rate_unit = $9, notes = $10, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $11
|
|
RETURNING *`,
|
|
[
|
|
productId,
|
|
customName,
|
|
brand,
|
|
categoryId,
|
|
productType,
|
|
activeIngredients,
|
|
description,
|
|
customRateAmount,
|
|
customRateUnit,
|
|
notes,
|
|
userProductId
|
|
]
|
|
);
|
|
|
|
const userProduct = result.rows[0];
|
|
|
|
// Handle spreader settings for granular products
|
|
if (spreaderSettings && Array.isArray(spreaderSettings) && productType === 'granular') {
|
|
// First, delete existing spreader settings for this user product
|
|
await pool.query(
|
|
'DELETE FROM product_spreader_settings WHERE user_product_id = $1',
|
|
[userProductId]
|
|
);
|
|
|
|
// Then add the new settings
|
|
for (const setting of spreaderSettings) {
|
|
// Check if equipment exists and belongs to user if equipmentId is provided
|
|
if (setting.equipmentId) {
|
|
const equipmentCheck = await pool.query(
|
|
'SELECT id FROM user_equipment WHERE id = $1 AND user_id = $2',
|
|
[setting.equipmentId, req.user.id]
|
|
);
|
|
|
|
if (equipmentCheck.rows.length === 0) {
|
|
throw new AppError(`Equipment with id ${setting.equipmentId} not found`, 404);
|
|
}
|
|
|
|
await pool.query(
|
|
`INSERT INTO product_spreader_settings
|
|
(user_product_id, equipment_id, setting_value, rate_description, notes)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
[userProductId, setting.equipmentId, setting.settingValue,
|
|
setting.rateDescription, setting.notes]
|
|
);
|
|
} else {
|
|
// Fall back to legacy brand/model approach
|
|
await pool.query(
|
|
`INSERT INTO product_spreader_settings
|
|
(user_product_id, spreader_brand, spreader_model, setting_value, rate_description, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
[
|
|
userProductId,
|
|
setting.spreaderBrand,
|
|
setting.spreaderModel,
|
|
setting.settingValue,
|
|
setting.rateDescription,
|
|
setting.notes
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Custom product updated successfully',
|
|
data: {
|
|
userProduct: {
|
|
id: userProduct.id,
|
|
baseProductId: userProduct.product_id,
|
|
customName: userProduct.custom_name,
|
|
customBrand: userProduct.custom_brand,
|
|
categoryId: userProduct.category_id,
|
|
customProductType: userProduct.custom_product_type,
|
|
customActiveIngredients: userProduct.custom_active_ingredients,
|
|
customDescription: userProduct.custom_description,
|
|
customRateAmount: parseFloat(userProduct.custom_rate_amount),
|
|
customRateUnit: userProduct.custom_rate_unit,
|
|
notes: userProduct.notes,
|
|
createdAt: userProduct.created_at,
|
|
updatedAt: userProduct.updated_at
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route DELETE /api/products/user/:id
|
|
// @desc Delete user's custom product
|
|
// @access Private
|
|
router.delete('/user/:id', validateParams(idParamSchema), async (req, res, next) => {
|
|
try {
|
|
const userProductId = req.params.id;
|
|
|
|
// Check if user product exists and belongs to user
|
|
const checkResult = await pool.query(
|
|
'SELECT id FROM user_products WHERE id = $1 AND user_id = $2',
|
|
[userProductId, req.user.id]
|
|
);
|
|
|
|
if (checkResult.rows.length === 0) {
|
|
throw new AppError('User product not found', 404);
|
|
}
|
|
|
|
// Check if product is used in any applications
|
|
const usageCheck = await pool.query(
|
|
`SELECT COUNT(*) as count
|
|
FROM application_plan_products
|
|
WHERE user_product_id = $1`,
|
|
[userProductId]
|
|
);
|
|
|
|
if (parseInt(usageCheck.rows[0].count) > 0) {
|
|
throw new AppError('Cannot delete product that has been used in applications', 400);
|
|
}
|
|
|
|
await pool.query('DELETE FROM user_products WHERE id = $1', [userProductId]);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: 'Custom product deleted successfully'
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// @route GET /api/products/search
|
|
// @desc Search products by name or ingredients
|
|
// @access Private
|
|
router.get('/search', async (req, res, next) => {
|
|
try {
|
|
const { q, limit = 10 } = req.query;
|
|
|
|
if (!q || q.length < 2) {
|
|
throw new AppError('Search query must be at least 2 characters', 400);
|
|
}
|
|
|
|
const result = await pool.query(
|
|
`SELECT p.*, pc.name as category_name
|
|
FROM products p
|
|
JOIN product_categories pc ON p.category_id = pc.id
|
|
WHERE p.name ILIKE $1 OR p.brand ILIKE $1 OR p.active_ingredients ILIKE $1
|
|
ORDER BY
|
|
CASE
|
|
WHEN p.name ILIKE $2 THEN 1
|
|
WHEN p.brand ILIKE $2 THEN 2
|
|
ELSE 3
|
|
END,
|
|
p.name
|
|
LIMIT $3`,
|
|
[`%${q}%`, `${q}%`, limit]
|
|
);
|
|
|
|
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
|
|
}))
|
|
}
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
module.exports = router; |