488 lines
No EOL
14 KiB
JavaScript
488 lines
No EOL
14 KiB
JavaScript
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;
|
|
}; |