1309 lines
34 KiB
JavaScript
1309 lines
34 KiB
JavaScript
const express = require("express");
|
|
const { query } = require("../config/database");
|
|
const { requireAuth } = require("../middleware/auth");
|
|
const { cache } = require("../middleware/cache");
|
|
const { apiLimiter } = require("../config/rateLimiter");
|
|
const {
|
|
invalidateProductCache,
|
|
invalidateBlogCache,
|
|
invalidatePortfolioCache,
|
|
invalidateHomepageCache,
|
|
invalidatePagesCache,
|
|
} = require("../utils/cacheInvalidation");
|
|
const logger = require("../config/logger");
|
|
const { asyncHandler } = require("../middleware/errorHandler");
|
|
const {
|
|
sendSuccess,
|
|
sendError,
|
|
sendNotFound,
|
|
} = require("../utils/responseHelpers");
|
|
const {
|
|
getById,
|
|
deleteById,
|
|
countRecords,
|
|
batchInsert,
|
|
getProductWithImages,
|
|
} = require("../utils/queryHelpers");
|
|
const { HTTP_STATUS } = require("../config/constants");
|
|
const router = express.Router();
|
|
|
|
// Apply rate limiting to all admin routes
|
|
router.use(apiLimiter);
|
|
|
|
// Dashboard stats API
|
|
router.get(
|
|
"/dashboard/stats",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const [productsCount, projectsCount, blogCount, pagesCount] =
|
|
await Promise.all([
|
|
countRecords("products"),
|
|
countRecords("portfolioprojects"),
|
|
countRecords("blogposts"),
|
|
countRecords("pages"),
|
|
]);
|
|
|
|
sendSuccess(res, {
|
|
stats: {
|
|
products: productsCount,
|
|
projects: projectsCount,
|
|
blog: blogCount,
|
|
pages: pagesCount,
|
|
},
|
|
user: {
|
|
name: req.session.name,
|
|
email: req.session.email,
|
|
role: req.session.role,
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Generic CRUD factory function
|
|
const createCRUDRoutes = (config) => {
|
|
const { table, resourceName, listFields = "*", requiresAuth = true } = config;
|
|
const auth = requiresAuth ? requireAuth : (req, res, next) => next();
|
|
|
|
// List all
|
|
router.get(
|
|
`/${resourceName}`,
|
|
auth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT ${listFields} FROM ${table} ORDER BY createdat DESC`,
|
|
);
|
|
sendSuccess(res, { [resourceName]: result.rows });
|
|
}),
|
|
);
|
|
|
|
// Get by ID
|
|
router.get(
|
|
`/${resourceName}/:id`,
|
|
auth,
|
|
asyncHandler(async (req, res) => {
|
|
const item = await getById(table, req.params.id);
|
|
if (!item) {
|
|
return sendNotFound(res, resourceName);
|
|
}
|
|
const responseKey = resourceName.slice(0, -1); // Remove 's' for singular
|
|
sendSuccess(res, { [responseKey]: item });
|
|
}),
|
|
);
|
|
|
|
// Delete
|
|
router.delete(
|
|
`/${resourceName}/:id`,
|
|
auth,
|
|
asyncHandler(async (req, res) => {
|
|
const deleted = await deleteById(table, req.params.id);
|
|
if (!deleted) {
|
|
return sendNotFound(res, resourceName);
|
|
}
|
|
sendSuccess(res, { message: `${resourceName} deleted successfully` });
|
|
}),
|
|
);
|
|
};
|
|
|
|
// Helper function to generate slug
|
|
const generateSlug = (name) => {
|
|
return name
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
.replace(/\s+/g, "-")
|
|
.replace(/-+/g, "-")
|
|
.trim();
|
|
};
|
|
|
|
// Products CRUD
|
|
router.get(
|
|
"/products",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT p.id, p.name, p.price, p.stockquantity, p.isactive, p.isfeatured,
|
|
p.isbestseller, p.category, p.createdat,
|
|
(SELECT COUNT(*) FROM product_images WHERE product_id = p.id) as image_count
|
|
FROM products p
|
|
ORDER BY p.createdat DESC`,
|
|
);
|
|
sendSuccess(res, { products: result.rows });
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
"/products/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
// Get product details
|
|
const product = await getById("products", req.params.id);
|
|
if (!product) {
|
|
return sendNotFound(res, "Product");
|
|
}
|
|
|
|
// Get associated images with color variants
|
|
const imagesResult = await query(
|
|
`SELECT id, image_url, color_variant, color_code, alt_text, display_order, is_primary, variant_price, variant_stock
|
|
FROM product_images
|
|
WHERE product_id = $1
|
|
ORDER BY display_order ASC, created_at ASC`,
|
|
[req.params.id],
|
|
);
|
|
|
|
product.images = imagesResult.rows;
|
|
sendSuccess(res, { product });
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
"/products",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
name,
|
|
shortdescription,
|
|
description,
|
|
price,
|
|
stockquantity,
|
|
category,
|
|
sku,
|
|
weight,
|
|
dimensions,
|
|
material,
|
|
isactive,
|
|
isfeatured,
|
|
isbestseller,
|
|
images,
|
|
} = req.body;
|
|
|
|
// Generate unique ID and slug from name
|
|
const productId =
|
|
"prod-" + Date.now() + "-" + Math.random().toString(36).substr(2, 9);
|
|
const slug = generateSlug(name);
|
|
|
|
// Insert product
|
|
const productResult = await query(
|
|
`INSERT INTO products (
|
|
id, name, slug, shortdescription, description, price, stockquantity,
|
|
category, sku, weight, dimensions, material, isactive, isfeatured,
|
|
isbestseller, createdat
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW())
|
|
RETURNING *`,
|
|
[
|
|
productId,
|
|
name,
|
|
slug,
|
|
shortdescription,
|
|
description,
|
|
price,
|
|
stockquantity || 0,
|
|
category,
|
|
sku,
|
|
weight,
|
|
dimensions,
|
|
material,
|
|
isactive !== false,
|
|
isfeatured || false,
|
|
isbestseller || false,
|
|
],
|
|
);
|
|
|
|
const product = productResult.rows[0];
|
|
|
|
// Insert images with color variants if provided (batch insert for performance)
|
|
if (images && Array.isArray(images) && images.length > 0) {
|
|
const imageRecords = images.map((img, i) => ({
|
|
product_id: product.id,
|
|
image_url: img.image_url,
|
|
color_variant: img.color_variant || null,
|
|
color_code: img.color_code || null,
|
|
alt_text: img.alt_text || name,
|
|
display_order: img.display_order !== undefined ? img.display_order : i,
|
|
is_primary: img.is_primary !== undefined ? img.is_primary : i === 0,
|
|
variant_price: img.variant_price || null,
|
|
variant_stock: img.variant_stock || 0,
|
|
}));
|
|
|
|
await batchInsert("product_images", imageRecords, [
|
|
"product_id",
|
|
"image_url",
|
|
"color_variant",
|
|
"color_code",
|
|
"alt_text",
|
|
"display_order",
|
|
"is_primary",
|
|
"variant_price",
|
|
"variant_stock",
|
|
]);
|
|
}
|
|
|
|
// Fetch complete product with images using helper
|
|
const completeProduct = await getProductWithImages(product.id);
|
|
|
|
// Invalidate product cache so new product appears immediately
|
|
invalidateProductCache();
|
|
|
|
sendSuccess(
|
|
res,
|
|
{
|
|
product: completeProduct,
|
|
message: "Product created successfully",
|
|
},
|
|
HTTP_STATUS.CREATED,
|
|
);
|
|
}),
|
|
);
|
|
|
|
router.put(
|
|
"/products/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
console.log("=== UPDATE PRODUCT API CALLED ===");
|
|
console.log("Product ID:", req.params.id);
|
|
console.log("Request body:", JSON.stringify(req.body, null, 2));
|
|
|
|
const {
|
|
name,
|
|
shortdescription,
|
|
description,
|
|
price,
|
|
stockquantity,
|
|
category,
|
|
sku,
|
|
weight,
|
|
dimensions,
|
|
material,
|
|
isactive,
|
|
isfeatured,
|
|
isbestseller,
|
|
images,
|
|
} = req.body;
|
|
|
|
console.log("Images to save:", images);
|
|
|
|
// Generate slug if name is provided
|
|
const slug = name ? generateSlug(name) : null;
|
|
|
|
// Build dynamic update query
|
|
const updates = [];
|
|
const values = [];
|
|
let paramIndex = 1;
|
|
|
|
if (name !== undefined) {
|
|
updates.push(`name = $${paramIndex++}`);
|
|
values.push(name);
|
|
updates.push(`slug = $${paramIndex++}`);
|
|
values.push(slug);
|
|
}
|
|
if (shortdescription !== undefined) {
|
|
updates.push(`shortdescription = $${paramIndex++}`);
|
|
values.push(shortdescription);
|
|
}
|
|
if (description !== undefined) {
|
|
updates.push(`description = $${paramIndex++}`);
|
|
values.push(description);
|
|
}
|
|
if (price !== undefined) {
|
|
updates.push(`price = $${paramIndex++}`);
|
|
values.push(price);
|
|
}
|
|
if (stockquantity !== undefined) {
|
|
updates.push(`stockquantity = $${paramIndex++}`);
|
|
values.push(stockquantity);
|
|
}
|
|
if (category !== undefined) {
|
|
updates.push(`category = $${paramIndex++}`);
|
|
values.push(category);
|
|
}
|
|
if (sku !== undefined) {
|
|
updates.push(`sku = $${paramIndex++}`);
|
|
values.push(sku);
|
|
}
|
|
if (weight !== undefined) {
|
|
updates.push(`weight = $${paramIndex++}`);
|
|
values.push(weight);
|
|
}
|
|
if (dimensions !== undefined) {
|
|
updates.push(`dimensions = $${paramIndex++}`);
|
|
values.push(dimensions);
|
|
}
|
|
if (material !== undefined) {
|
|
updates.push(`material = $${paramIndex++}`);
|
|
values.push(material);
|
|
}
|
|
if (isactive !== undefined) {
|
|
updates.push(`isactive = $${paramIndex++}`);
|
|
values.push(isactive);
|
|
}
|
|
if (isfeatured !== undefined) {
|
|
updates.push(`isfeatured = $${paramIndex++}`);
|
|
values.push(isfeatured);
|
|
}
|
|
if (isbestseller !== undefined) {
|
|
updates.push(`isbestseller = $${paramIndex++}`);
|
|
values.push(isbestseller);
|
|
}
|
|
|
|
updates.push(`updatedat = NOW()`);
|
|
values.push(req.params.id);
|
|
|
|
const updateQuery = `UPDATE products SET ${updates.join(
|
|
", ",
|
|
)} WHERE id = $${paramIndex} RETURNING *`;
|
|
const result = await query(updateQuery, values);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Product");
|
|
}
|
|
|
|
console.log("Product updated in database:", result.rows[0].id);
|
|
|
|
// Update images if provided
|
|
if (images && Array.isArray(images)) {
|
|
console.log("Updating images, count:", images.length);
|
|
|
|
// Delete existing images for this product
|
|
const deleteResult = await query(
|
|
"DELETE FROM product_images WHERE product_id = $1",
|
|
[req.params.id],
|
|
);
|
|
console.log("Deleted existing images, count:", deleteResult.rowCount);
|
|
|
|
// Insert new images (batch insert for performance)
|
|
const imageRecords = images.map((img, i) => ({
|
|
product_id: req.params.id,
|
|
image_url: img.image_url,
|
|
color_variant: img.color_variant || null,
|
|
color_code: img.color_code || null,
|
|
alt_text: img.alt_text || result.rows[0].name,
|
|
display_order: img.display_order !== undefined ? img.display_order : i,
|
|
is_primary: img.is_primary !== undefined ? img.is_primary : i === 0,
|
|
variant_price: img.variant_price || null,
|
|
variant_stock: img.variant_stock || 0,
|
|
}));
|
|
|
|
await batchInsert("product_images", imageRecords, [
|
|
"product_id",
|
|
"image_url",
|
|
"color_variant",
|
|
"color_code",
|
|
"alt_text",
|
|
"display_order",
|
|
"is_primary",
|
|
"variant_price",
|
|
"variant_stock",
|
|
]);
|
|
console.log("All images inserted successfully (batch)");
|
|
} else {
|
|
console.log("No images to update");
|
|
}
|
|
|
|
// Fetch complete product with images using helper
|
|
const completeProduct = await getProductWithImages(req.params.id);
|
|
|
|
console.log("Final product with images:", completeProduct);
|
|
console.log("=== PRODUCT UPDATE COMPLETE ===");
|
|
|
|
// Invalidate product cache
|
|
invalidateProductCache();
|
|
|
|
sendSuccess(res, {
|
|
product: completeProduct,
|
|
message: "Product updated successfully",
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.delete(
|
|
"/products/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const productId = req.params.id;
|
|
console.log(`[DELETE] Attempting to delete product with ID: ${productId}`);
|
|
|
|
// First, check if product exists
|
|
const existingProduct = await getById("products", productId);
|
|
if (!existingProduct) {
|
|
console.log(`[DELETE] Product not found with ID: ${productId}`);
|
|
return sendNotFound(res, "Product");
|
|
}
|
|
|
|
console.log(`[DELETE] Found product: ${existingProduct.name}`);
|
|
|
|
// Product images will be deleted automatically via CASCADE
|
|
const deleted = await deleteById("products", productId);
|
|
console.log(`[DELETE] Delete result: ${deleted}`);
|
|
|
|
if (!deleted) {
|
|
console.log(`[DELETE] Failed to delete product: ${productId}`);
|
|
return sendNotFound(res, "Product");
|
|
}
|
|
|
|
// Invalidate product cache so deletion reflects immediately
|
|
invalidateProductCache();
|
|
console.log(`[DELETE] Product deleted successfully: ${productId}`);
|
|
sendSuccess(res, { message: "Product deleted successfully" });
|
|
}),
|
|
);
|
|
|
|
// Portfolio Projects CRUD
|
|
router.get(
|
|
"/portfolio/projects",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT id, title, description, imageurl, category, isactive, createdat FROM portfolioprojects ORDER BY createdat DESC",
|
|
);
|
|
sendSuccess(res, { projects: result.rows });
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
"/portfolio/projects/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const project = await getById("portfolioprojects", req.params.id);
|
|
if (!project) {
|
|
return sendNotFound(res, "Project");
|
|
}
|
|
sendSuccess(res, { project });
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
"/portfolio/projects",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { title, description, category, isactive, imageurl, images } =
|
|
req.body;
|
|
const imagesJson =
|
|
images && images.length > 0 ? JSON.stringify(images) : "[]";
|
|
const result = await query(
|
|
`INSERT INTO portfolioprojects (id, title, description, category, isactive, imageurl, images, createdat)
|
|
VALUES ('portfolio-' || gen_random_uuid()::text, $1, $2, $3, $4, $5, $6, NOW()) RETURNING *`,
|
|
[
|
|
title,
|
|
description,
|
|
category,
|
|
isactive !== false,
|
|
imageurl || null,
|
|
imagesJson,
|
|
],
|
|
);
|
|
|
|
// Invalidate portfolio cache
|
|
invalidatePortfolioCache();
|
|
|
|
sendSuccess(
|
|
res,
|
|
{
|
|
project: result.rows[0],
|
|
message: "Project created successfully",
|
|
},
|
|
HTTP_STATUS.CREATED,
|
|
);
|
|
}),
|
|
);
|
|
|
|
router.put(
|
|
"/portfolio/projects/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { title, description, category, isactive, imageurl, images } =
|
|
req.body;
|
|
const imagesJson =
|
|
images && images.length > 0 ? JSON.stringify(images) : "[]";
|
|
const result = await query(
|
|
`UPDATE portfolioprojects
|
|
SET title = $1, description = $2, category = $3, isactive = $4, imageurl = $5, images = $6, updatedat = NOW()
|
|
WHERE id = $7 RETURNING *`,
|
|
[
|
|
title,
|
|
description,
|
|
category,
|
|
isactive !== false,
|
|
imageurl || null,
|
|
imagesJson,
|
|
req.params.id,
|
|
],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Project");
|
|
}
|
|
|
|
// Invalidate portfolio cache
|
|
invalidatePortfolioCache();
|
|
|
|
sendSuccess(res, {
|
|
project: result.rows[0],
|
|
message: "Project updated successfully",
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.delete(
|
|
"/portfolio/projects/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const deleted = await deleteById("portfolioprojects", req.params.id);
|
|
if (!deleted) {
|
|
return sendNotFound(res, "Project");
|
|
}
|
|
// Invalidate portfolio cache
|
|
invalidatePortfolioCache();
|
|
sendSuccess(res, { message: "Project deleted successfully" });
|
|
}),
|
|
);
|
|
|
|
// Blog Posts CRUD
|
|
router.get(
|
|
"/blog",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT id, title, slug, excerpt, ispublished, createdat FROM blogposts ORDER BY createdat DESC",
|
|
);
|
|
sendSuccess(res, { posts: result.rows });
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
"/blog/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const post = await getById("blogposts", req.params.id);
|
|
if (!post) {
|
|
return sendNotFound(res, "Blog post");
|
|
}
|
|
sendSuccess(res, { post });
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
"/blog",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
title,
|
|
slug,
|
|
excerpt,
|
|
content,
|
|
featuredimage,
|
|
images,
|
|
videourl,
|
|
poll,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished,
|
|
} = req.body;
|
|
const result = await query(
|
|
`INSERT INTO blogposts (id, title, slug, excerpt, content, featuredimage, imageurl, images, videourl, poll, metatitle, metadescription, ispublished, createdat)
|
|
VALUES ('blog-' || gen_random_uuid()::text, $1, $2, $3, $4, $5, $5, $6, $7, $8, $9, $10, $11, NOW()) RETURNING *`,
|
|
[
|
|
title,
|
|
slug,
|
|
excerpt,
|
|
content,
|
|
featuredimage || null,
|
|
images || "[]",
|
|
videourl || null,
|
|
poll || null,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished || false,
|
|
],
|
|
);
|
|
|
|
// Invalidate blog cache
|
|
invalidateBlogCache();
|
|
|
|
sendSuccess(
|
|
res,
|
|
{
|
|
post: result.rows[0],
|
|
message: "Blog post created successfully",
|
|
},
|
|
HTTP_STATUS.CREATED,
|
|
);
|
|
}),
|
|
);
|
|
|
|
router.put(
|
|
"/blog/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
title,
|
|
slug,
|
|
excerpt,
|
|
content,
|
|
featuredimage,
|
|
images,
|
|
videourl,
|
|
poll,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished,
|
|
} = req.body;
|
|
const result = await query(
|
|
`UPDATE blogposts
|
|
SET title = $1, slug = $2, excerpt = $3, content = $4, featuredimage = $5, imageurl = $5,
|
|
images = $6, videourl = $7, poll = $8, metatitle = $9, metadescription = $10,
|
|
ispublished = $11, updatedat = NOW()
|
|
WHERE id = $12 RETURNING *`,
|
|
[
|
|
title,
|
|
slug,
|
|
excerpt,
|
|
content,
|
|
featuredimage || null,
|
|
images || "[]",
|
|
videourl || null,
|
|
poll || null,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished || false,
|
|
req.params.id,
|
|
],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Blog post");
|
|
}
|
|
|
|
// Invalidate blog cache
|
|
invalidateBlogCache();
|
|
|
|
sendSuccess(res, {
|
|
post: result.rows[0],
|
|
message: "Blog post updated successfully",
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.delete(
|
|
"/blog/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const deleted = await deleteById("blogposts", req.params.id);
|
|
if (!deleted) {
|
|
return sendNotFound(res, "Blog post");
|
|
}
|
|
// Invalidate blog cache
|
|
invalidateBlogCache();
|
|
sendSuccess(res, { message: "Blog post deleted successfully" });
|
|
}),
|
|
);
|
|
|
|
// Custom Pages CRUD
|
|
router.get(
|
|
"/pages",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT id, title, slug, ispublished, createdat FROM pages ORDER BY createdat DESC",
|
|
);
|
|
sendSuccess(res, { pages: result.rows });
|
|
}),
|
|
);
|
|
|
|
router.get(
|
|
"/pages/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const page = await getById("pages", req.params.id);
|
|
if (!page) {
|
|
return sendNotFound(res, "Page");
|
|
}
|
|
sendSuccess(res, { page });
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
"/pages",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
title,
|
|
slug,
|
|
content,
|
|
contenthtml,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished,
|
|
pagedata,
|
|
} = req.body;
|
|
|
|
// Generate readable ID from slug
|
|
const pageId = `page-${slug}`;
|
|
|
|
const result = await query(
|
|
`INSERT INTO pages (id, title, slug, content, pagecontent, metatitle, metadescription, ispublished, isactive, pagedata, createdat)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) RETURNING *`,
|
|
[
|
|
pageId,
|
|
title,
|
|
slug,
|
|
content,
|
|
contenthtml || content,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished !== false,
|
|
ispublished !== false,
|
|
pagedata ? JSON.stringify(pagedata) : null,
|
|
],
|
|
);
|
|
|
|
// Invalidate pages cache
|
|
invalidatePagesCache();
|
|
|
|
sendSuccess(
|
|
res,
|
|
{
|
|
page: result.rows[0],
|
|
message: "Page created successfully",
|
|
},
|
|
HTTP_STATUS.CREATED,
|
|
);
|
|
}),
|
|
);
|
|
|
|
router.put(
|
|
"/pages/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
title,
|
|
slug,
|
|
content,
|
|
contenthtml,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished,
|
|
pagedata,
|
|
} = req.body;
|
|
|
|
console.log("=== PAGE UPDATE REQUEST ===");
|
|
console.log("Page ID:", req.params.id);
|
|
console.log("Slug:", slug);
|
|
console.log("Title:", title);
|
|
console.log(
|
|
"PageData:",
|
|
pagedata ? JSON.stringify(pagedata).substring(0, 200) + "..." : "null",
|
|
);
|
|
|
|
const result = await query(
|
|
`UPDATE pages
|
|
SET title = $1, slug = $2, content = $3, pagecontent = $4, metatitle = $5,
|
|
metadescription = $6, ispublished = $7, isactive = $8, pagedata = $9, updatedat = NOW()
|
|
WHERE id = $10 RETURNING *`,
|
|
[
|
|
title,
|
|
slug,
|
|
content,
|
|
contenthtml || content,
|
|
metatitle,
|
|
metadescription,
|
|
ispublished !== false,
|
|
ispublished !== false,
|
|
pagedata ? JSON.stringify(pagedata) : null,
|
|
req.params.id,
|
|
],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Page");
|
|
}
|
|
|
|
console.log("=== PAGE UPDATED SUCCESSFULLY ===");
|
|
console.log(
|
|
"Updated pagedata:",
|
|
result.rows[0].pagedata
|
|
? JSON.stringify(result.rows[0].pagedata).substring(0, 200) + "..."
|
|
: "null",
|
|
);
|
|
|
|
// Invalidate pages cache
|
|
invalidatePagesCache();
|
|
|
|
sendSuccess(res, {
|
|
page: result.rows[0],
|
|
message: "Page updated successfully",
|
|
});
|
|
}),
|
|
);
|
|
|
|
router.delete(
|
|
"/pages/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const deleted = await deleteById("pages", req.params.id);
|
|
if (!deleted) {
|
|
return sendNotFound(res, "Page");
|
|
}
|
|
// Invalidate pages cache
|
|
invalidatePagesCache();
|
|
sendSuccess(res, { message: "Page deleted successfully" });
|
|
}),
|
|
);
|
|
|
|
// Homepage Settings - custom handler with cache invalidation
|
|
router.get(
|
|
"/homepage/settings",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT settings FROM site_settings WHERE key = $1",
|
|
["homepage"],
|
|
);
|
|
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
|
|
sendSuccess(res, { settings });
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
"/homepage/settings",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const settings = req.body;
|
|
await query(
|
|
`INSERT INTO site_settings (key, settings, updatedat)
|
|
VALUES ($1, $2, NOW())
|
|
ON CONFLICT (key) DO UPDATE SET settings = $2, updatedat = NOW()`,
|
|
["homepage", JSON.stringify(settings)],
|
|
);
|
|
|
|
// Invalidate homepage cache
|
|
invalidateHomepageCache();
|
|
|
|
sendSuccess(res, { message: "Homepage settings saved successfully" });
|
|
}),
|
|
);
|
|
|
|
// General Settings
|
|
const settingsHandler = (key) => ({
|
|
get: asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT settings FROM site_settings WHERE key = $1",
|
|
[key],
|
|
);
|
|
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
|
|
sendSuccess(res, { settings });
|
|
}),
|
|
post: asyncHandler(async (req, res) => {
|
|
const newSettings = req.body;
|
|
// Get existing settings first and merge
|
|
const existingResult = await query(
|
|
"SELECT settings FROM site_settings WHERE key = $1",
|
|
[key],
|
|
);
|
|
const existingSettings =
|
|
existingResult.rows.length > 0 ? existingResult.rows[0].settings : {};
|
|
// Merge new settings with existing (new settings overwrite existing for same keys)
|
|
const mergedSettings = { ...existingSettings, ...newSettings };
|
|
|
|
await query(
|
|
`INSERT INTO site_settings (key, settings, updatedat)
|
|
VALUES ($1, $2, NOW())
|
|
ON CONFLICT (key) DO UPDATE SET settings = $2, updatedat = NOW()`,
|
|
[key, JSON.stringify(mergedSettings)],
|
|
);
|
|
sendSuccess(res, { message: `${key} settings saved successfully` });
|
|
}),
|
|
});
|
|
|
|
const generalSettings = settingsHandler("general");
|
|
router.get("/settings", requireAuth, generalSettings.get);
|
|
router.post("/settings", requireAuth, generalSettings.post);
|
|
|
|
// Menu Management
|
|
router.get(
|
|
"/menu",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT settings FROM site_settings WHERE key = 'menu'",
|
|
);
|
|
const items =
|
|
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
|
|
sendSuccess(res, { items });
|
|
}),
|
|
);
|
|
|
|
router.post(
|
|
"/menu",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { items } = req.body;
|
|
await query(
|
|
`INSERT INTO site_settings (key, settings, updatedat)
|
|
VALUES ('menu', $1, NOW())
|
|
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
|
|
[JSON.stringify({ items })],
|
|
);
|
|
sendSuccess(res, { message: "Menu saved successfully" });
|
|
}),
|
|
);
|
|
|
|
// ==================== TEAM MEMBERS CRUD ====================
|
|
|
|
// Get all team members
|
|
router.get(
|
|
"/team-members",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"SELECT * FROM team_members ORDER BY display_order ASC, created_at DESC",
|
|
);
|
|
sendSuccess(res, { teamMembers: result.rows });
|
|
}),
|
|
);
|
|
|
|
// Get single team member
|
|
router.get(
|
|
"/team-members/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query("SELECT * FROM team_members WHERE id = $1", [
|
|
req.params.id,
|
|
]);
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Team member");
|
|
}
|
|
sendSuccess(res, { teamMember: result.rows[0] });
|
|
}),
|
|
);
|
|
|
|
// Create team member
|
|
router.post(
|
|
"/team-members",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { name, position, bio, image_url, display_order } = req.body;
|
|
|
|
if (!name || !position) {
|
|
return sendError(
|
|
res,
|
|
"Name and position are required",
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
const result = await query(
|
|
`INSERT INTO team_members (name, position, bio, image_url, display_order, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)
|
|
RETURNING *`,
|
|
[name, position, bio || null, image_url || null, display_order || 0],
|
|
);
|
|
|
|
sendSuccess(
|
|
res,
|
|
{
|
|
teamMember: result.rows[0],
|
|
message: "Team member created successfully",
|
|
},
|
|
HTTP_STATUS.CREATED,
|
|
);
|
|
}),
|
|
);
|
|
|
|
// Update team member
|
|
router.put(
|
|
"/team-members/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { name, position, bio, image_url, display_order } = req.body;
|
|
|
|
if (!name || !position) {
|
|
return sendError(
|
|
res,
|
|
"Name and position are required",
|
|
HTTP_STATUS.BAD_REQUEST,
|
|
);
|
|
}
|
|
|
|
const result = await query(
|
|
`UPDATE team_members
|
|
SET name = $1, position = $2, bio = $3, image_url = $4, display_order = $5, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $6
|
|
RETURNING *`,
|
|
[name, position, bio, image_url, display_order || 0, req.params.id],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Team member");
|
|
}
|
|
|
|
sendSuccess(res, {
|
|
teamMember: result.rows[0],
|
|
message: "Team member updated successfully",
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Delete team member
|
|
router.delete(
|
|
"/team-members/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
"DELETE FROM team_members WHERE id = $1 RETURNING *",
|
|
[req.params.id],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Team member");
|
|
}
|
|
|
|
sendSuccess(res, { message: "Team member deleted successfully" });
|
|
}),
|
|
);
|
|
|
|
// ===========================
|
|
// CUSTOMERS MANAGEMENT
|
|
// ===========================
|
|
|
|
// Get all customers (for newsletter management)
|
|
router.get(
|
|
"/customers",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const {
|
|
page = 1,
|
|
limit = 50,
|
|
newsletter = "all",
|
|
search = "",
|
|
status = "all",
|
|
} = req.query;
|
|
const offset = (parseInt(page) - 1) * parseInt(limit);
|
|
|
|
let whereClause = "WHERE 1=1";
|
|
const params = [];
|
|
let paramIndex = 1;
|
|
|
|
// Filter by verification status
|
|
if (status === "verified") {
|
|
whereClause += " AND email_verified = TRUE";
|
|
} else if (status === "unverified") {
|
|
whereClause += " AND email_verified = FALSE";
|
|
}
|
|
// 'all' shows everyone
|
|
|
|
// Filter by newsletter subscription
|
|
if (newsletter === "subscribed") {
|
|
whereClause += " AND newsletter_subscribed = TRUE";
|
|
} else if (newsletter === "unsubscribed") {
|
|
whereClause += " AND newsletter_subscribed = FALSE";
|
|
}
|
|
|
|
// Search by name or email
|
|
if (search) {
|
|
whereClause += ` AND (
|
|
LOWER(first_name) LIKE $${paramIndex} OR
|
|
LOWER(last_name) LIKE $${paramIndex} OR
|
|
LOWER(email) LIKE $${paramIndex}
|
|
)`;
|
|
params.push(`%${search.toLowerCase()}%`);
|
|
paramIndex++;
|
|
}
|
|
|
|
// Get total count
|
|
const countResult = await query(
|
|
`SELECT COUNT(*) FROM customers ${whereClause}`,
|
|
params,
|
|
);
|
|
const total = parseInt(countResult.rows[0].count);
|
|
|
|
// Get customers with cart and wishlist counts
|
|
params.push(parseInt(limit), offset);
|
|
const result = await query(
|
|
`SELECT c.id, c.first_name, c.last_name, c.email, c.newsletter_subscribed,
|
|
c.last_login, c.login_count, c.created_at, c.is_active, c.email_verified,
|
|
COALESCE((SELECT SUM(quantity) FROM customer_cart WHERE customer_id = c.id), 0) as cart_count,
|
|
COALESCE((SELECT COUNT(*) FROM customer_wishlist WHERE customer_id = c.id), 0) as wishlist_count
|
|
FROM customers c
|
|
${whereClause.replace(/customers/g, "c")}
|
|
ORDER BY c.created_at DESC
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
params,
|
|
);
|
|
|
|
sendSuccess(res, {
|
|
customers: result.rows,
|
|
pagination: {
|
|
total,
|
|
page: parseInt(page),
|
|
limit: parseInt(limit),
|
|
totalPages: Math.ceil(total / parseInt(limit)),
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Get single customer details
|
|
router.get(
|
|
"/customers/:id",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT id, first_name, last_name, email, newsletter_subscribed,
|
|
email_verified, last_login, login_count, created_at, updated_at, is_active
|
|
FROM customers WHERE id = $1`,
|
|
[req.params.id],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Customer");
|
|
}
|
|
|
|
sendSuccess(res, { customer: result.rows[0] });
|
|
}),
|
|
);
|
|
|
|
// Update customer status (activate/deactivate)
|
|
router.patch(
|
|
"/customers/:id/status",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const { is_active } = req.body;
|
|
|
|
const result = await query(
|
|
`UPDATE customers SET is_active = $1, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $2 RETURNING id, first_name, last_name, email, is_active`,
|
|
[is_active, req.params.id],
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
return sendNotFound(res, "Customer");
|
|
}
|
|
|
|
sendSuccess(res, {
|
|
message: `Customer ${
|
|
is_active ? "activated" : "deactivated"
|
|
} successfully`,
|
|
customer: result.rows[0],
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Export customers for newsletter (CSV format data)
|
|
router.get(
|
|
"/customers/export/newsletter",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT first_name, last_name, email
|
|
FROM customers
|
|
WHERE email_verified = TRUE AND newsletter_subscribed = TRUE AND is_active = TRUE
|
|
ORDER BY created_at DESC`,
|
|
);
|
|
|
|
// Return as JSON array - frontend can convert to CSV if needed
|
|
sendSuccess(res, {
|
|
customers: result.rows,
|
|
count: result.rows.length,
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Get customer statistics
|
|
router.get(
|
|
"/customers/stats/overview",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const [totalResult, verifiedResult, newsletterResult, activeResult] =
|
|
await Promise.all([
|
|
query("SELECT COUNT(*) FROM customers"),
|
|
query("SELECT COUNT(*) FROM customers WHERE email_verified = TRUE"),
|
|
query(
|
|
"SELECT COUNT(*) FROM customers WHERE newsletter_subscribed = TRUE AND email_verified = TRUE",
|
|
),
|
|
query(
|
|
"SELECT COUNT(*) FROM customers WHERE is_active = TRUE AND email_verified = TRUE",
|
|
),
|
|
]);
|
|
|
|
sendSuccess(res, {
|
|
stats: {
|
|
total: parseInt(totalResult.rows[0].count),
|
|
verified: parseInt(verifiedResult.rows[0].count),
|
|
newsletterSubscribed: parseInt(newsletterResult.rows[0].count),
|
|
active: parseInt(activeResult.rows[0].count),
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Get customer's cart items (admin view)
|
|
router.get(
|
|
"/customers/:id/cart",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT cc.id, cc.product_id, cc.quantity, cc.variant_color, cc.variant_size, cc.added_at,
|
|
p.name, p.price, p.imageurl
|
|
FROM customer_cart cc
|
|
JOIN products p ON p.id = cc.product_id
|
|
WHERE cc.customer_id = $1
|
|
ORDER BY cc.added_at DESC`,
|
|
[req.params.id],
|
|
);
|
|
|
|
const items = result.rows.map((row) => ({
|
|
id: row.id,
|
|
productId: row.product_id,
|
|
name: row.name,
|
|
price: parseFloat(row.price),
|
|
image: row.imageurl,
|
|
quantity: row.quantity,
|
|
variantColor: row.variant_color,
|
|
variantSize: row.variant_size,
|
|
addedAt: row.added_at,
|
|
}));
|
|
|
|
const total = items.reduce(
|
|
(sum, item) => sum + item.price * item.quantity,
|
|
0,
|
|
);
|
|
|
|
sendSuccess(res, {
|
|
items,
|
|
itemCount: items.length,
|
|
total: total.toFixed(2),
|
|
});
|
|
}),
|
|
);
|
|
|
|
// Get customer's wishlist items (admin view)
|
|
router.get(
|
|
"/customers/:id/wishlist",
|
|
requireAuth,
|
|
asyncHandler(async (req, res) => {
|
|
const result = await query(
|
|
`SELECT cw.id, cw.product_id, cw.added_at,
|
|
p.name, p.price, p.imageurl
|
|
FROM customer_wishlist cw
|
|
JOIN products p ON p.id = cw.product_id
|
|
WHERE cw.customer_id = $1
|
|
ORDER BY cw.added_at DESC`,
|
|
[req.params.id],
|
|
);
|
|
|
|
const items = result.rows.map((row) => ({
|
|
id: row.id,
|
|
productId: row.product_id,
|
|
name: row.name,
|
|
price: parseFloat(row.price),
|
|
image: row.imageurl,
|
|
addedAt: row.added_at,
|
|
}));
|
|
|
|
sendSuccess(res, {
|
|
items,
|
|
itemCount: items.length,
|
|
});
|
|
}),
|
|
);
|
|
|
|
module.exports = router;
|