const express = require("express"); const { query } = require("../config/database"); const { requireAuth } = require("../middleware/auth"); const { cache } = require("../middleware/cache"); const { invalidateProductCache, invalidateBlogCache, invalidatePortfolioCache, invalidateHomepageCache, } = require("../utils/cacheInvalidation"); const logger = require("../config/logger"); const { asyncHandler } = require("../middleware/errorHandler"); const { sendSuccess, sendError, sendNotFound, } = require("../utils/responseHelpers"); const { getById, deleteById, countRecords } = require("../utils/queryHelpers"); const { HTTP_STATUS } = require("../config/constants"); const router = express.Router(); // 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, alt_text, display_order, is_primary 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 if (images && Array.isArray(images) && images.length > 0) { for (let i = 0; i < images.length; i++) { const img = images[i]; await query( `INSERT INTO product_images ( product_id, image_url, color_variant, color_code, alt_text, display_order, is_primary, variant_price, variant_stock ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ product.id, img.image_url, img.color_variant || null, img.color_code || null, img.alt_text || name, img.display_order || i, img.is_primary || i === 0, img.variant_price || null, img.variant_stock || 0, ] ); } } // Fetch complete product with images const completeProduct = await query( `SELECT p.*, json_agg( json_build_object( 'id', pi.id, 'image_url', pi.image_url, 'color_variant', pi.color_variant, 'color_code', pi.color_code, 'alt_text', pi.alt_text, 'display_order', pi.display_order, 'is_primary', pi.is_primary, 'variant_price', pi.variant_price, 'variant_stock', pi.variant_stock ) ORDER BY pi.display_order ) FILTER (WHERE pi.id IS NOT NULL) as images FROM products p LEFT JOIN product_images pi ON pi.product_id = p.id WHERE p.id = $1 GROUP BY p.id`, [product.id] ); sendSuccess( res, { product: completeProduct.rows[0], 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 for (let i = 0; i < images.length; i++) { const img = images[i]; console.log( `Inserting image ${i + 1}/${images.length}:`, img.image_url ); await query( `INSERT INTO product_images ( product_id, image_url, color_variant, color_code, alt_text, display_order, is_primary, variant_price, variant_stock ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ req.params.id, img.image_url, img.color_variant || null, img.color_code || null, img.alt_text || result.rows[0].name, img.display_order || i, img.is_primary || i === 0, img.variant_price || null, img.variant_stock || 0, ] ); } console.log("All images inserted successfully"); } else { console.log("No images to update"); } // Fetch complete product with images const completeProduct = await query( `SELECT p.*, json_agg( json_build_object( 'id', pi.id, 'image_url', pi.image_url, 'color_variant', pi.color_variant, 'color_code', pi.color_code, 'alt_text', pi.alt_text, 'display_order', pi.display_order, 'is_primary', pi.is_primary, 'variant_price', pi.variant_price, 'variant_stock', pi.variant_stock ) ORDER BY pi.display_order ) FILTER (WHERE pi.id IS NOT NULL) as images FROM products p LEFT JOIN product_images pi ON pi.product_id = p.id WHERE p.id = $1 GROUP BY p.id`, [req.params.id] ); console.log("Final product with images:", completeProduct.rows[0]); console.log("=== PRODUCT UPDATE COMPLETE ==="); // Invalidate product cache invalidateProductCache(); sendSuccess(res, { product: completeProduct.rows[0], message: "Product updated successfully", }); }) ); router.delete( "/products/:id", requireAuth, asyncHandler(async (req, res) => { // Product images will be deleted automatically via CASCADE const deleted = await deleteById("products", req.params.id); if (!deleted) { return sendNotFound(res, "Product"); } 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 } = req.body; const result = await query( `INSERT INTO portfolioprojects (title, description, category, isactive, imageurl, createdat) VALUES ($1, $2, $3, $4, $5, NOW()) RETURNING *`, [title, description, category, isactive !== false, imageurl || null] ); 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 } = req.body; const result = await query( `UPDATE portfolioprojects SET title = $1, description = $2, category = $3, isactive = $4, imageurl = $5, updatedat = NOW() WHERE id = $6 RETURNING *`, [ title, description, category, isactive !== false, imageurl || null, req.params.id, ] ); if (result.rows.length === 0) { return sendNotFound(res, "Project"); } 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"); } 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, metatitle, metadescription, ispublished, } = req.body; const result = await query( `INSERT INTO blogposts (title, slug, excerpt, content, metatitle, metadescription, ispublished, createdat) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`, [ title, slug, excerpt, content, metatitle, metadescription, ispublished || false, ] ); 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, metatitle, metadescription, ispublished, } = req.body; const result = await query( `UPDATE blogposts SET title = $1, slug = $2, excerpt = $3, content = $4, metatitle = $5, metadescription = $6, ispublished = $7, updatedat = NOW() WHERE id = $8 RETURNING *`, [ title, slug, excerpt, content, metatitle, metadescription, ispublished || false, req.params.id, ] ); if (result.rows.length === 0) { return sendNotFound(res, "Blog post"); } 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"); } 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, ] ); 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; 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"); } 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"); } sendSuccess(res, { message: "Page deleted successfully" }); }) ); // Settings Management 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` }); }), }); // Homepage Settings const homepageSettings = settingsHandler("homepage"); router.get("/homepage/settings", requireAuth, homepageSettings.get); router.post("/homepage/settings", requireAuth, homepageSettings.post); // General Settings 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" }); }) ); module.exports = router;