fea/nhat-dat-11042026-merge #1

Closed
minhnhat wants to merge 27 commits from UKSOURCE/cms.hailearning.edu.vn:fea/nhat-dat-11042026-merge into fea/nhat-13042028-merge-kiet-thien
6 changed files with 135 additions and 3 deletions
Showing only changes of commit b6f1b92feb - Show all commits

View File

@@ -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 });
}
};

View File

@@ -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);

View File

@@ -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);

View 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 };

View File

@@ -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">

View File

@@ -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;