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 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()`, [key, JSON.stringify(settings)], ); 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;