diff --git a/backend/public/uploads/products/wilson-1745952400108-890575617.jpg b/backend/public/uploads/products/wilson-1745952400108-890575617.jpg deleted file mode 100644 index 10487c6..0000000 Binary files a/backend/public/uploads/products/wilson-1745952400108-890575617.jpg and /dev/null differ diff --git a/backend/public/uploads/products/wilson-1745952603966-106384496.jpg b/backend/public/uploads/products/wilson-1745952603966-106384496.jpg deleted file mode 100644 index 10487c6..0000000 Binary files a/backend/public/uploads/products/wilson-1745952603966-106384496.jpg and /dev/null differ diff --git a/backend/src/routes/productAdmin.js b/backend/src/routes/productAdmin.js index fc79556..1e63be5 100644 --- a/backend/src/routes/productAdmin.js +++ b/backend/src/routes/productAdmin.js @@ -9,150 +9,79 @@ module.exports = (pool, query, 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' + // Check for array + let products = []; + if (req.body.products) { + products = req.body.products; + } else { + const { + name, + description, + categoryName, + price, + stockQuantity, + weightGrams, + lengthCm, + widthCm, + heightCm, + stockNotification, + origin, + age, + materialType, + color, + images, + tags + } = req.body; + + products.push({ + name, + description, + categoryName, + price, + stockQuantity, + weightGrams, + lengthCm, + widthCm, + heightCm, + stockNotification, + origin, + age, + materialType, + color, + images: images || [], + tags }); } + + // Prepare promises array for concurrent execution + const productPromises = products.map(product => createProduct(product, pool)); - // Begin transaction - const client = await pool.connect(); + // Execute all product creation promises concurrently + const results = await Promise.all(productPromises); - 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(); - } + // Separate successes and errors + const success = results.filter(result => !result.error).map(result => ({ + message: 'Product created successfully', + product: result.product, + code: 201 + })); + + const errs = results.filter(result => result.error); + + res.status(201).json({ + message: errs.length > 0 ? + (success.length > 0 ? 'Some Products failed to create, check errors array' : "Failed to create Products") : + 'Products created successfully', + success, + errs + }); } catch (error) { next(error); } }); router.post('/:id/stock-notification', async (req, res, next) => { + const client = await pool.connect(); try { const { id } = req.params; const { enabled, email, threshold } = req.body; @@ -164,6 +93,7 @@ module.exports = (pool, query, authMiddleware) => { message: 'Admin access required' }); } + await client.query('BEGIN'); // Check if product exists const productCheck = await query( @@ -186,22 +116,21 @@ module.exports = (pool, query, authMiddleware) => { }; // Update product with notification settings - const result = await query( - `UPDATE products - SET stock_notification = $1 - WHERE id = $2 - RETURNING *`, - [JSON.stringify(notificationSettings), id] - ); + const result = await addThresholdNotification(id, enabled, email, threshold, client); + await client.query('COMMIT'); res.json({ message: 'Stock notification settings updated successfully', product: result.rows[0] }); - } catch (error) { + } catch (error) { + await client.query('ROLLBACK'); next(error); + } finally { + client.release(); } }); + // Update an existing product router.put('/:id', async (req, res, next) => { @@ -215,6 +144,7 @@ module.exports = (pool, query, authMiddleware) => { stockQuantity, weightGrams, lengthCm, + stockNotification, widthCm, heightCm, origin, @@ -344,6 +274,10 @@ module.exports = (pool, query, authMiddleware) => { updateValues.push(color); valueIndex++; } + + if (stockNotification) { + await addThresholdNotification(id, stockNotification.enabled, stockNotification.email, stockNotification.threshold, client); + } if (updateFields.length > 0) { const updateQuery = ` @@ -485,4 +419,179 @@ module.exports = (pool, query, authMiddleware) => { }); return router; -}; \ No newline at end of file +}; + + +async function addThresholdNotification(id, enabled, email, threshold ,client) { + // Store notification settings as JSONB + const notificationSettings = { + enabled, + email: email || null, + threshold: threshold || 0 + }; + + // Update product with notification settings + const result = await client.query( + `UPDATE products + SET stock_notification = $1 + WHERE id = $2 + RETURNING *`, + [JSON.stringify(notificationSettings), id] + ); + return result; + +} + +async function createProduct(product, pool) { + const { + name, + description, + categoryName, + price, + stockQuantity, + weightGrams, + lengthCm, + widthCm, + heightCm, + stockNotification, + origin, + age, + materialType, + color, + images, + tags + } = product; + + // Validate required fields + if (!name || !description || !categoryName || !price || !stockQuantity) { + return { + error: true, + message: 'Required fields missing: name, description, categoryName, price, and stockQuantity are mandatory', + code: 400 + }; + } + + // Get a client from the pool for this specific product + 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 { + error: true, + message: `Category "${categoryName}" not found`, + code: 404 + }; + } + + 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) { + const imagePromises = images.map((image, i) => { + const { path, isPrimary = (i === 0) } = image; + return client.query( + 'INSERT INTO product_images (product_id, image_path, display_order, is_primary) VALUES ($1, $2, $3, $4)', + [productId, path, i, isPrimary] + ); + }); + + await Promise.all(imagePromises); + } + + // 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] + ); + } + } + + // 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 productResult = await client.query(productQuery, [productId]); + + // Add threshold notification if provided + if (stockNotification) { + await addThresholdNotification(productId, stockNotification.enabled, stockNotification.email, stockNotification.threshold, client); + } + + + await client.query('COMMIT'); + return { + error: false, + product: productResult.rows[0] + }; + } catch (error) { + await client.query('ROLLBACK'); + return { + error: true, + message: `Error creating product: ${error.message}`, + code: 500 + }; + } finally { + client.release(); + } +} \ No newline at end of file diff --git a/start.sh b/start.sh index f8d0fce..399421c 100755 --- a/start.sh +++ b/start.sh @@ -10,7 +10,7 @@ if [ "$MODE" == "cloud" ]; then sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=cloud/' ./backend/.env # Start with cloud profile - docker compose --profile cloud up -d + docker compose --profile cloud up -d --build else echo "Starting in SELF-HOSTED mode" # Make sure .env has self-hosted settings @@ -18,6 +18,6 @@ else sed -i 's/DEPLOYMENT_MODE=.*/DEPLOYMENT_MODE=self-hosted/' ./backend/.env # Start without extra services - docker compose up -d + docker compose up -d --build fi echo "Deployment complete in $MODE mode" \ No newline at end of file