From b6f1b92feb78347ce777c2fb17a58efbcbe55283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=E1=BB=91ng=20Th=C3=A0nh=20=C4=90=E1=BA=A1t?= <84076965+tongthanhdat009@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:57:28 +0700 Subject: [PATCH] feat(header-menu): add maintenance mode functionality and related UI elements --- controllers/headerMenuController.js | 70 ++++++++++++++++++- models/headerMenu.js | 5 ++ routes/index.js | 1 + ..._08_200000_add_header_menu_maintainance.js | 46 ++++++++++++ views/admin/header/index.ejs | 11 +++ views/admin/header/menu.ejs | 5 ++ 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 scripts/2026_04_08_200000_add_header_menu_maintainance.js 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 @@ +