const express = require('express'); const { v4: uuidv4 } = require('uuid'); const router = express.Router(); module.exports = (pool, query, authMiddleware) => { // Apply authentication middleware to all routes router.use(authMiddleware); // Create a new product with multiple images router.post('/', async (req, res, next) => { try { const { name, description, categoryName, price, stockQuantity, weightGrams, lengthCm, widthCm, heightCm, origin, age, materialType, color, images, tags } = req.body; // Validate required fields if (!name || !description || !categoryName || !price || !stockQuantity) { return res.status(400).json({ error: true, message: 'Required fields missing: name, description, categoryName, price, and stockQuantity are mandatory' }); } // Begin transaction const client = await pool.connect(); try { await client.query('BEGIN'); // Get category ID by name const categoryResult = await client.query( 'SELECT id FROM product_categories WHERE name = $1', [categoryName] ); if (categoryResult.rows.length === 0) { return res.status(404).json({ error: true, message: `Category "${categoryName}" not found` }); } const categoryId = categoryResult.rows[0].id; // Create product const productId = uuidv4(); await client.query( `INSERT INTO products ( id, name, description, category_id, price, stock_quantity, weight_grams, length_cm, width_cm, height_cm, origin, age, material_type, color ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)`, [ productId, name, description, categoryId, price, stockQuantity, weightGrams || null, lengthCm || null, widthCm || null, heightCm || null, origin || null, age || null, materialType || null, color || null ] ); // Add images if provided if (images && images.length > 0) { for (let i = 0; i < images.length; i++) { const { path, isPrimary = (i === 0) } = images[i]; await client.query( 'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)', [productId, path, i, isPrimary] ); } } // Add tags if provided if (tags && tags.length > 0) { for (const tagName of tags) { // Get tag ID let tagResult = await client.query( 'SELECT id FROM tags WHERE name = $1', [tagName] ); let tagId; // If tag doesn't exist, create it if (tagResult.rows.length === 0) { const newTagResult = await client.query( 'INSERT INTO tags (name) VALUES ($1) RETURNING id', [tagName] ); tagId = newTagResult.rows[0].id; } else { tagId = tagResult.rows[0].id; } // Add tag to product await client.query( 'INSERT INTO product_tags (product_id, tag_id) VALUES ($1, $2)', [productId, tagId] ); } } await client.query('COMMIT'); // Get complete product with images and tags const productQuery = ` SELECT p.*, pc.name as category_name, ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, json_agg(json_build_object( 'id', pi.id, 'path', pi.image_path, 'isPrimary', pi.is_primary, 'displayOrder', pi.display_order )) FILTER (WHERE pi.id IS NOT NULL) AS images FROM products p JOIN product_categories pc ON p.category_id = pc.id LEFT JOIN product_tags pt ON p.id = pt.product_id LEFT JOIN tags t ON pt.tag_id = t.id LEFT JOIN product_images pi ON p.id = pi.product_id WHERE p.id = $1 GROUP BY p.id, pc.name `; const product = await query(productQuery, [productId]); res.status(201).json({ message: 'Product created successfully', product: product.rows[0] }); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } catch (error) { next(error); } }); router.post('/:id/stock-notification', async (req, res, next) => { try { const { id } = req.params; const { enabled, email, threshold } = req.body; // Check if user is admin if (!req.user.is_admin) { return res.status(403).json({ error: true, message: 'Admin access required' }); } // Check if product exists const productCheck = await query( 'SELECT * FROM products WHERE id = $1', [id] ); if (productCheck.rows.length === 0) { return res.status(404).json({ error: true, message: 'Product not found' }); } // Store notification settings as JSONB const notificationSettings = { enabled, email: email || null, threshold: threshold || 0 }; // Update product with notification settings const result = await query( `UPDATE products SET stock_notification = $1 WHERE id = $2 RETURNING *`, [JSON.stringify(notificationSettings), id] ); res.json({ message: 'Stock notification settings updated successfully', product: result.rows[0] }); } catch (error) { next(error); } }); // Update an existing product router.put('/:id', async (req, res, next) => { try { const { id } = req.params; const { name, description, categoryName, price, stockQuantity, weightGrams, lengthCm, widthCm, heightCm, origin, age, materialType, color, images, tags } = req.body; // Begin transaction const client = await pool.connect(); try { await client.query('BEGIN'); // Check if product exists const productCheck = await client.query( 'SELECT * FROM products WHERE id = $1', [id] ); if (productCheck.rows.length === 0) { return res.status(404).json({ error: true, message: 'Product not found' }); } // If category is changing, get the new category ID let categoryId = productCheck.rows[0].category_id; if (categoryName) { const categoryResult = await client.query( 'SELECT id FROM product_categories WHERE name = $1', [categoryName] ); if (categoryResult.rows.length === 0) { return res.status(404).json({ error: true, message: `Category "${categoryName}" not found` }); } categoryId = categoryResult.rows[0].id; } // Update product const updateFields = []; const updateValues = []; let valueIndex = 1; if (name) { updateFields.push(`name = $${valueIndex}`); updateValues.push(name); valueIndex++; } if (description) { updateFields.push(`description = $${valueIndex}`); updateValues.push(description); valueIndex++; } if (categoryName) { updateFields.push(`category_id = $${valueIndex}`); updateValues.push(categoryId); valueIndex++; } if (price) { updateFields.push(`price = $${valueIndex}`); updateValues.push(price); valueIndex++; } if (stockQuantity !== undefined) { updateFields.push(`stock_quantity = $${valueIndex}`); updateValues.push(stockQuantity); valueIndex++; } if (weightGrams !== undefined) { updateFields.push(`weight_grams = $${valueIndex}`); updateValues.push(weightGrams); valueIndex++; } if (lengthCm !== undefined) { updateFields.push(`length_cm = $${valueIndex}`); updateValues.push(lengthCm); valueIndex++; } if (widthCm !== undefined) { updateFields.push(`width_cm = $${valueIndex}`); updateValues.push(widthCm); valueIndex++; } if (heightCm !== undefined) { updateFields.push(`height_cm = $${valueIndex}`); updateValues.push(heightCm); valueIndex++; } if (origin !== undefined) { updateFields.push(`origin = $${valueIndex}`); updateValues.push(origin); valueIndex++; } if (age !== undefined) { updateFields.push(`age = $${valueIndex}`); updateValues.push(age); valueIndex++; } if (materialType !== undefined) { updateFields.push(`material_type = $${valueIndex}`); updateValues.push(materialType); valueIndex++; } if (color !== undefined) { updateFields.push(`color = $${valueIndex}`); updateValues.push(color); valueIndex++; } if (updateFields.length > 0) { const updateQuery = ` UPDATE products SET ${updateFields.join(', ')} WHERE id = $${valueIndex} `; updateValues.push(id); await client.query(updateQuery, updateValues); } // Update images if provided if (images) { // Remove existing images await client.query( 'DELETE FROM product_images WHERE product_id = $1', [id] ); // Add new images for (let i = 0; i < images.length; i++) { const { path, isPrimary = (i === 0) } = images[i]; await client.query( 'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)', [id, path, i, isPrimary] ); } } // Update tags if provided if (tags) { // Remove existing tags await client.query( 'DELETE FROM product_tags WHERE product_id = $1', [id] ); // Add new tags for (const tagName of tags) { // Get tag ID let tagResult = await client.query( 'SELECT id FROM tags WHERE name = $1', [tagName] ); let tagId; // If tag doesn't exist, create it if (tagResult.rows.length === 0) { const newTagResult = await client.query( 'INSERT INTO tags (name) VALUES ($1) RETURNING id', [tagName] ); tagId = newTagResult.rows[0].id; } else { tagId = tagResult.rows[0].id; } // Add tag to product await client.query( 'INSERT INTO product_tags (product_id, tag_id) VALUES ($1, $2)', [id, tagId] ); } } await client.query('COMMIT'); // Get updated product const productQuery = ` SELECT p.*, pc.name as category_name, ARRAY_AGG(DISTINCT t.name) FILTER (WHERE t.name IS NOT NULL) AS tags, json_agg(json_build_object( 'id', pi.id, 'path', pi.image_path, 'isPrimary', pi.is_primary, 'displayOrder', pi.display_order )) FILTER (WHERE pi.id IS NOT NULL) AS images FROM products p JOIN product_categories pc ON p.category_id = pc.id LEFT JOIN product_tags pt ON p.id = pt.product_id LEFT JOIN tags t ON pt.tag_id = t.id LEFT JOIN product_images pi ON p.id = pi.product_id WHERE p.id = $1 GROUP BY p.id, pc.name `; const product = await query(productQuery, [id]); res.json({ message: 'Product updated successfully', product: product.rows[0] }); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } } catch (error) { next(error); } }); // Delete a product router.delete('/:id', async (req, res, next) => { try { const { id } = req.params; // Check if product exists const productCheck = await query( 'SELECT * FROM products WHERE id = $1', [id] ); if (productCheck.rows.length === 0) { return res.status(404).json({ error: true, message: 'Product not found' }); } // Delete product (cascade will handle related records) await query( 'DELETE FROM products WHERE id = $1', [id] ); res.json({ message: 'Product deleted successfully' }); } catch (error) { next(error); } }); return router; };