forked from UKSOURCE/cms.hailearning.edu.vn
fea/nhat-dat-11042026-merge #1
@@ -1,6 +1,36 @@
|
|||||||
const HeaderMenu = require("../models/headerMenu");
|
const HeaderMenu = require("../models/headerMenu");
|
||||||
const slugify = require("slugify");
|
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
|
* Helper: Build tree structure from flat array
|
||||||
*/
|
*/
|
||||||
@@ -19,8 +49,10 @@ const buildMenuTree = (items, parentId = null, isPublic = false) => {
|
|||||||
cleanItem = {
|
cleanItem = {
|
||||||
id: item._id,
|
id: item._id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
url: item.url,
|
url: item.is_maintainance ? "/maintenance" : item.url,
|
||||||
|
originalUrl: item.url,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
|
is_maintainance: Boolean(item.is_maintainance),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +91,7 @@ exports.store = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
console.log("=== BACKEND: store hit ===");
|
console.log("=== BACKEND: store hit ===");
|
||||||
console.log("Body:", req.body);
|
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 slug = slugify(title, { lower: true, strict: true });
|
||||||
|
|
||||||
const newItem = new HeaderMenu({
|
const newItem = new HeaderMenu({
|
||||||
@@ -70,6 +102,7 @@ exports.store = async (req, res) => {
|
|||||||
order: order || 0,
|
order: order || 0,
|
||||||
status: status || "active",
|
status: status || "active",
|
||||||
type: type || "internal",
|
type: type || "internal",
|
||||||
|
is_maintainance: parseBooleanFlag(is_maintainance),
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedItem = await newItem.save();
|
const savedItem = await newItem.save();
|
||||||
@@ -101,7 +134,7 @@ exports.update = async (req, res) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
console.log("=== BACKEND: update hit ===", { id });
|
console.log("=== BACKEND: update hit ===", { id });
|
||||||
console.log("Body:", req.body);
|
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 = {
|
const updateData = {
|
||||||
url,
|
url,
|
||||||
@@ -109,6 +142,7 @@ exports.update = async (req, res) => {
|
|||||||
order,
|
order,
|
||||||
status,
|
status,
|
||||||
type,
|
type,
|
||||||
|
is_maintainance: parseBooleanFlag(is_maintainance),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
@@ -203,3 +237,33 @@ exports.api = async (req, res) => {
|
|||||||
res.status(500).json({ success: false, message: error.message });
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ const HeaderMenuSchema = new mongoose.Schema({
|
|||||||
enum: ['active', 'inactive'],
|
enum: ['active', 'inactive'],
|
||||||
default: 'active'
|
default: 'active'
|
||||||
},
|
},
|
||||||
|
is_maintainance: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ['internal', 'external'],
|
enum: ['internal', 'external'],
|
||||||
@@ -43,6 +47,7 @@ const HeaderMenuSchema = new mongoose.Schema({
|
|||||||
// Indexes for optimization
|
// Indexes for optimization
|
||||||
HeaderMenuSchema.index({ order: 1 });
|
HeaderMenuSchema.index({ order: 1 });
|
||||||
HeaderMenuSchema.index({ status: 1 });
|
HeaderMenuSchema.index({ status: 1 });
|
||||||
|
HeaderMenuSchema.index({ is_maintainance: 1 });
|
||||||
HeaderMenuSchema.index({ parentId: 1, order: 1 });
|
HeaderMenuSchema.index({ parentId: 1, order: 1 });
|
||||||
|
|
||||||
module.exports = mongoose.model('HeaderMenu', HeaderMenuSchema);
|
module.exports = mongoose.model('HeaderMenu', HeaderMenuSchema);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ router.get("/api/menu-tree", headerController.getMenuTreeAPI);
|
|||||||
|
|
||||||
// Header Menu New Module API
|
// Header Menu New Module API
|
||||||
router.get("/api/header-menu", headerMenuController.api);
|
router.get("/api/header-menu", headerMenuController.api);
|
||||||
|
router.get("/api/header-menu/maintenance", headerMenuController.maintenanceStatus);
|
||||||
|
|
||||||
// Social Links API routes
|
// Social Links API routes
|
||||||
router.get("/api/social-links", socialLinkController.index);
|
router.get("/api/social-links", socialLinkController.index);
|
||||||
|
|||||||
46
scripts/2026_04_08_200000_add_header_menu_maintainance.js
Normal file
46
scripts/2026_04_08_200000_add_header_menu_maintainance.js
Normal file
@@ -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 };
|
||||||
@@ -1067,6 +1067,17 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium d-block">Maintenance Mode</label>
|
||||||
|
<input type="hidden" name="is_maintainance" value="false">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" name="is_maintainance" id="formMaintainance" value="true">
|
||||||
|
<label class="form-check-label" for="formMaintainance">
|
||||||
|
Redirect this menu page to the maintenance screen while it is being repaired.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Use this when the linked page should be temporarily unavailable to visitors.</small>
|
||||||
|
</div>
|
||||||
<div class="mb-0">
|
<div class="mb-0">
|
||||||
<label class="form-label fw-medium">Link Type</label>
|
<label class="form-label fw-medium">Link Type</label>
|
||||||
<div class="d-flex gap-3">
|
<div class="d-flex gap-3">
|
||||||
|
|||||||
@@ -42,6 +42,9 @@
|
|||||||
<% } else { %>
|
<% } else { %>
|
||||||
<span class="badge bg-soft-success ms-2">Active</span>
|
<span class="badge bg-soft-success ms-2">Active</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (item.is_maintainance) { %>
|
||||||
|
<span class="badge ms-2 bg-warning text-dark">Maintenance</span>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted small text-truncate" style="max-width: 300px;">
|
<div class="text-muted small text-truncate" style="max-width: 300px;">
|
||||||
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
|
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
|
||||||
@@ -192,6 +195,7 @@
|
|||||||
document.getElementById('formUrl').value = '';
|
document.getElementById('formUrl').value = '';
|
||||||
document.getElementById('formOrder').value = '0';
|
document.getElementById('formOrder').value = '0';
|
||||||
document.getElementById('formStatus').value = 'active';
|
document.getElementById('formStatus').value = 'active';
|
||||||
|
document.getElementById('formMaintainance').checked = false;
|
||||||
document.getElementById('typeInternal').checked = true;
|
document.getElementById('typeInternal').checked = true;
|
||||||
|
|
||||||
const modalElement = document.getElementById('modalAddMenu');
|
const modalElement = document.getElementById('modalAddMenu');
|
||||||
@@ -216,6 +220,7 @@
|
|||||||
document.getElementById('formUrl').value = item.url;
|
document.getElementById('formUrl').value = item.url;
|
||||||
document.getElementById('formOrder').value = item.order;
|
document.getElementById('formOrder').value = item.order;
|
||||||
document.getElementById('formStatus').value = item.status;
|
document.getElementById('formStatus').value = item.status;
|
||||||
|
document.getElementById('formMaintainance').checked = Boolean(item.is_maintainance);
|
||||||
|
|
||||||
if (item.type === 'external') {
|
if (item.type === 'external') {
|
||||||
document.getElementById('typeExternal').checked = true;
|
document.getElementById('typeExternal').checked = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user