diff --git a/controllers/headerMenuController.js b/controllers/headerMenuController.js index 0c1ddfb..07074e5 100644 --- a/controllers/headerMenuController.js +++ b/controllers/headerMenuController.js @@ -1,6 +1,36 @@ 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 */ @@ -19,8 +49,10 @@ const buildMenuTree = (items, parentId = null, isPublic = false) => { cleanItem = { id: item._id, title: item.title, - url: item.url, + url: item.is_maintainance ? "/maintenance" : item.url, + originalUrl: item.url, type: item.type, + is_maintainance: Boolean(item.is_maintainance), }; } @@ -59,7 +91,7 @@ exports.store = async (req, res) => { try { console.log("=== BACKEND: store hit ==="); console.log("Body:", req.body); - const { title, url, parentId, order, status, type } = 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({ @@ -70,6 +102,7 @@ exports.store = async (req, res) => { order: order || 0, status: status || "active", type: type || "internal", + is_maintainance: parseBooleanFlag(is_maintainance), }); const savedItem = await newItem.save(); @@ -101,7 +134,7 @@ exports.update = async (req, res) => { const { id } = req.params; console.log("=== BACKEND: update hit ===", { id }); console.log("Body:", req.body); - const { title, url, parentId, order, status, type } = req.body; + const { title, url, parentId, order, status, type, is_maintainance } = req.body; const updateData = { url, @@ -109,6 +142,7 @@ exports.update = async (req, res) => { order, status, type, + is_maintainance: parseBooleanFlag(is_maintainance), }; if (title) { @@ -203,3 +237,33 @@ exports.api = async (req, res) => { 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 }); + } +}; diff --git a/models/headerMenu.js b/models/headerMenu.js index 13c8d3c..613030f 100644 --- a/models/headerMenu.js +++ b/models/headerMenu.js @@ -31,6 +31,10 @@ const HeaderMenuSchema = new mongoose.Schema({ enum: ['active', 'inactive'], default: 'active' }, + is_maintainance: { + type: Boolean, + default: false + }, type: { type: String, enum: ['internal', 'external'], @@ -43,6 +47,7 @@ const HeaderMenuSchema = new mongoose.Schema({ // Indexes for optimization HeaderMenuSchema.index({ order: 1 }); HeaderMenuSchema.index({ status: 1 }); +HeaderMenuSchema.index({ is_maintainance: 1 }); HeaderMenuSchema.index({ parentId: 1, order: 1 }); module.exports = mongoose.model('HeaderMenu', HeaderMenuSchema); diff --git a/routes/index.js b/routes/index.js index 48e1d8c..39deb0d 100644 --- a/routes/index.js +++ b/routes/index.js @@ -52,6 +52,7 @@ router.get("/api/menu-tree", headerController.getMenuTreeAPI); // Header Menu New Module API router.get("/api/header-menu", headerMenuController.api); +router.get("/api/header-menu/maintenance", headerMenuController.maintenanceStatus); // Social Links API routes router.get("/api/social-links", socialLinkController.index); diff --git a/scripts/2026_04_08_200000_add_header_menu_maintainance.js b/scripts/2026_04_08_200000_add_header_menu_maintainance.js new file mode 100644 index 0000000..69c3e95 --- /dev/null +++ b/scripts/2026_04_08_200000_add_header_menu_maintainance.js @@ -0,0 +1,46 @@ +const mongoose = require("mongoose"); +const connectDB = require("../config/database"); + +async function up() { + await connectDB(); + + try { + const collection = mongoose.connection.db.collection("headermenus"); + const result = await collection.updateMany( + { is_maintainance: { $exists: false } }, + { $set: { is_maintainance: false } }, + ); + + console.log( + `Added is_maintainance=false to ${result.modifiedCount || 0} HeaderMenu document(s).`, + ); + } catch (error) { + console.error("Failed to add is_maintainance to HeaderMenu documents:", error); + throw error; + } finally { + await mongoose.disconnect(); + } +} + +async function down() { + await connectDB(); + + try { + const collection = mongoose.connection.db.collection("headermenus"); + const result = await collection.updateMany( + { is_maintainance: { $exists: true } }, + { $unset: { is_maintainance: "" } }, + ); + + console.log( + `Removed is_maintainance from ${result.modifiedCount || 0} HeaderMenu document(s).`, + ); + } catch (error) { + console.error("Failed to rollback is_maintainance on HeaderMenu documents:", error); + throw error; + } finally { + await mongoose.disconnect(); + } +} + +module.exports = { up, down }; diff --git a/views/admin/header/index.ejs b/views/admin/header/index.ejs index 03cdb93..3e868d3 100644 --- a/views/admin/header/index.ejs +++ b/views/admin/header/index.ejs @@ -1067,6 +1067,17 @@ +
+ + +
+ + +
+ Use this when the linked page should be temporarily unavailable to visitors. +
diff --git a/views/admin/header/menu.ejs b/views/admin/header/menu.ejs index 1aa6c0b..3429c5e 100644 --- a/views/admin/header/menu.ejs +++ b/views/admin/header/menu.ejs @@ -42,6 +42,9 @@ <% } else { %> Active <% } %> + <% if (item.is_maintainance) { %> + Maintenance + <% } %>
<%= item.url %> @@ -192,6 +195,7 @@ document.getElementById('formUrl').value = ''; document.getElementById('formOrder').value = '0'; document.getElementById('formStatus').value = 'active'; + document.getElementById('formMaintainance').checked = false; document.getElementById('typeInternal').checked = true; const modalElement = document.getElementById('modalAddMenu'); @@ -216,6 +220,7 @@ document.getElementById('formUrl').value = item.url; document.getElementById('formOrder').value = item.order; document.getElementById('formStatus').value = item.status; + document.getElementById('formMaintainance').checked = Boolean(item.is_maintainance); if (item.type === 'external') { document.getElementById('typeExternal').checked = true;