const HeaderMenu = require("../models/headerMenu"); const slugify = require("slugify"); const parseBooleanFlag = (value) => { if (typeof value === "boolean") { return value; } if (typeof value === "string") { const normalized = value.trim().toLowerCase(); return ["true", "1", "on", "yes"].includes(normalized); } return false; }; const normalizeInternalUrl = (url = "") => { if (typeof url !== "string") { return null; } const trimmed = url.trim(); if (!trimmed || !trimmed.startsWith("/")) { return null; } if (trimmed === "/") { return "/"; } return trimmed.replace(/\/+$/, ""); }; /** * Helper: Build tree structure from flat array */ const buildMenuTree = (items, parentId = null, isPublic = false) => { const branch = []; const children = items.filter( (item) => String(item.parentId) === String(parentId) || (item.parentId === null && parentId === null), ); for (const child of children) { const item = child.toObject ? child.toObject() : { ...child }; // Clean data for public API if requested let cleanItem = item; if (isPublic) { cleanItem = { id: item._id, title: item.title, url: item.is_maintainance ? "/maintenance" : item.url, originalUrl: item.url, type: item.type, is_maintainance: Boolean(item.is_maintainance), }; } const subChildren = buildMenuTree(items, item._id, isPublic); cleanItem.children = subChildren.length > 0 ? subChildren : []; branch.push(cleanItem); } return branch.sort((a, b) => a.order - b.order); }; /** * Helper: Recursive delete children */ const deleteRecursive = async (parentId) => { const children = await HeaderMenu.find({ parentId }); for (const child of children) { await deleteRecursive(child._id); await HeaderMenu.findByIdAndDelete(child._id); } }; // 1. Render Menu Tab logic exports.index = async (req, res) => { try { const items = await HeaderMenu.find().sort({ order: 1 }); const menuTree = buildMenuTree(items); return { menuTree, flatItems: items }; } catch (error) { console.error("Error fetching menu items:", error); throw error; } }; // 2. Create Menu Item exports.store = async (req, res) => { try { console.log("=== BACKEND: store hit ==="); console.log("Body:", req.body); const { title, url, parentId, order, status, type, is_maintainance } = req.body; const slug = slugify(title, { lower: true, strict: true }); const newItem = new HeaderMenu({ title, slug, url, parentId: parentId || null, order: order || 0, status: status || "active", type: type || "internal", is_maintainance: parseBooleanFlag(is_maintainance), }); const savedItem = await newItem.save(); console.log("=== MENU CREATED ===", savedItem); // Return JSON for AJAX requests if (req.xhr || req.headers.accept?.indexOf("json") > -1) { return res.json({ success: true, message: "Menu item created successfully", data: savedItem }); } req.flash("success_msg", "Menu item created successfully"); res.redirect("/admin/header?tab=menu"); } catch (error) { console.error("=== CREATE MENU ERROR ===", error); // Return JSON for AJAX requests if (req.xhr || req.headers.accept?.indexOf("json") > -1) { return res.status(400).json({ success: false, message: error.message }); } req.flash("error_msg", "Failed to create menu item: " + error.message); res.redirect("/admin/header?tab=menu"); } }; // 3. Update Menu Item exports.update = async (req, res) => { try { const { id } = req.params; console.log("=== BACKEND: update hit ===", { id }); console.log("Body:", req.body); const { title, url, parentId, order, status, type, is_maintainance } = req.body; const updateData = { url, parentId: parentId || null, order, status, type, is_maintainance: parseBooleanFlag(is_maintainance), }; if (title) { updateData.title = title; updateData.slug = slugify(title, { lower: true, strict: true }); } const updated = await HeaderMenu.findByIdAndUpdate(id, updateData, { new: true }); if (!updated) { console.log("=== UPDATE MENU NOT FOUND ===", id); // Return JSON for AJAX requests if (req.xhr || req.headers.accept?.indexOf("json") > -1) { return res.status(404).json({ success: false, message: "Menu item not found" }); } req.flash("error_msg", "Menu item not found"); } else { console.log("=== MENU UPDATED ===", updated); // Return JSON for AJAX requests if (req.xhr || req.headers.accept?.indexOf("json") > -1) { return res.json({ success: true, message: "Menu item updated successfully", data: updated }); } req.flash("success_msg", "Menu item updated successfully"); } res.redirect("/admin/header?tab=menu"); } catch (error) { console.error("=== UPDATE MENU ERROR ===", error); // Return JSON for AJAX requests if (req.xhr || req.headers.accept?.indexOf("json") > -1) { return res.status(400).json({ success: false, message: error.message }); } req.flash("error_msg", "Update failed: " + error.message); res.redirect("/admin/header?tab=menu"); } }; // 4. Delete Menu Item (Cascade delete children) exports.destroy = async (req, res) => { try { const { id } = req.body; const menuId = id || req.params.id; console.log("=== BACKEND: destroy hit ===", { menuId, body: req.body }); await deleteRecursive(menuId); await HeaderMenu.findByIdAndDelete(menuId); console.log("=== MENU DELETED ===", menuId); req.flash("success_msg", "Menu item and its sub-menu deleted successfully"); res.redirect("/admin/header?tab=menu"); } catch (error) { console.error("=== DELETE MENU ERROR ===", error); req.flash("error_msg", "Delete failed: " + error.message); res.redirect("/admin/header?tab=menu"); } }; // 5. Reorder Menu exports.reorder = async (req, res) => { try { const { items } = req.body; // Array of { id, order, parentId } if (items && Array.isArray(items)) { const bulkOps = items.map((item) => ({ updateOne: { filter: { _id: item.id }, update: { order: item.order, parentId: item.parentId || null }, }, })); await HeaderMenu.bulkWrite(bulkOps); return res.json({ success: true, message: "Reordered successfully" }); } res.status(400).json({ success: false, message: "Invalid data" }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }; // Public API: Get active menu as clean tree exports.api = async (req, res) => { try { const items = await HeaderMenu.find({ status: "active" }).sort({ order: 1 }); const tree = buildMenuTree(items, null, true); res.json({ success: true, data: tree }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } }; exports.maintenanceStatus = async (req, res) => { try { const items = await HeaderMenu.find({ status: "active", is_maintainance: true, }) .select("title url slug") .sort({ order: 1 }) .lean(); const urls = [...new Set(items.map((item) => normalizeInternalUrl(item.url)).filter(Boolean))]; res.json({ success: true, data: { enabled: items.length > 0, urls, items: items.map((item) => ({ id: String(item._id), title: item.title, slug: item.slug, url: item.url, })), }, }); } catch (error) { res.status(500).json({ success: false, message: error.message }); } };