Refactor: remove unused APIs, controllers, models and data files. Update: icon picker all system, Dashboard, Main UI more respone, Change: Logo, favicon.

This commit is contained in:
2026-04-23 18:44:37 +07:00
parent 8ce6cf3c7e
commit 4ba3eb4a82
48 changed files with 438 additions and 14850 deletions
+6 -3
View File
@@ -65,10 +65,13 @@ exports.index = async (req, res) => {
data[s] = data[s] || defaults[s];
});
const frontendUrl = process.env.FRONTEND_URL || "";
return res.render("admin/about/index", {
layout: "layouts/main",
title: "About Management",
data,
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
@@ -114,17 +117,17 @@ exports.update = async (req, res) => {
if (!hasChanges) {
req.flash("info_msg", "No changes were made");
return req.session.save(() => res.redirect("/admin/about-us"));
return req.session.save(() => res.redirect("/admin/about"));
}
await doc.save();
req.flash("success_msg", "About page configuration has been updated!");
return req.session.save(() => res.redirect("/admin/about-us"));
return req.session.save(() => res.redirect("/admin/about"));
} catch (err) {
console.error("About update error:", err);
req.flash("error_msg", `Update error: ${err.message}`);
return req.session.save(() => res.redirect("/admin/about-us"));
return req.session.save(() => res.redirect("/admin/about"));
}
};
File diff suppressed because it is too large Load Diff
-154
View File
@@ -1,154 +0,0 @@
const Home = require("../models/home");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
// Helper to get FAQ data from Home model
const getFaqData = async () => {
const home = await Home.findOne().sort({ updatedAt: -1 });
if (!home || !home.faq) {
return {
heading: "",
subheading: "",
description: "",
items: [],
ctaButton: { label: "", href: "" },
};
}
return home.faq.toObject ? home.faq.toObject() : home.faq;
};
// API to get FAQ data for frontend
exports.api = async (req, res) => {
try {
const faqData = await getFaqData();
return res.json(faqData);
} catch (err) {
console.error("API Error:", err);
res.status(500).json({ error: "Error loading FAQ data" });
}
};
// Method for legacy route compatibility or internal use
exports.getFAQData = async (req, res) => {
return exports.api(req, res);
};
// Render admin view
exports.index = async (req, res) => {
try {
const data = await getFaqData();
// Ensure default structure if data is partial
const safeData = {
heading: data.heading || "",
subheading: data.subheading || "",
description: data.description || "",
ctaButton: data.ctaButton || { label: "", href: "" },
items: data.items || [],
};
const frontendUrl = process.env.FRONTEND_URL;
res.render("admin/home/faq/index", {
title: "FAQ Section Management",
layout: "layouts/main",
data: safeData,
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error("Error in FAQ index:", error);
req.flash("error_msg", "An error occurred while loading the page");
res.redirect("/admin/dashboard");
}
};
// Update FAQ data
exports.update = async (req, res) => {
try {
const { heading, subheading, description, ctaLabel, ctaHref, items } =
req.body;
let parsedItems = [];
if (items) {
try {
parsedItems = typeof items === "string" ? JSON.parse(items) : items;
} catch (e) {
console.error("Error parsing items JSON:", e);
parsedItems = [];
}
}
let home = await Home.findOne().sort({ updatedAt: -1 });
if (!home) {
home = new Home({});
}
// ✅ Capture BEFORE state
const beforeData = home.faq
? JSON.parse(
JSON.stringify(home.faq.toObject ? home.faq.toObject() : home.faq),
)
: {};
const updatedFaqData = {
heading: heading || "",
subheading: subheading || "",
description: description || "",
ctaButton: {
label: ctaLabel || "",
href: ctaHref || "",
},
items: parsedItems.map((item) => ({
question: item.question || "",
answer: item.answer || "",
})),
};
home.faq = updatedFaqData;
await home.save();
// ✅ Capture AFTER state
const afterData = JSON.parse(JSON.stringify(updatedFaqData));
// ✅ AUDIT LOGGING - FAQ Updated
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Home",
documentId: home._id,
action: AUDIT_ACTIONS.UPDATE_FAQ,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "FAQ section updated successfully");
res.redirect("/admin/home/faq");
} catch (err) {
console.error("Error updating FAQ:", err);
req.flash("error_msg", err.message || "Error updating FAQ");
res.redirect("/admin/home/faq");
}
};
// Placeholder methods to prevent route crashes if routes are not cleaned up immediately
exports.addFAQ = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.updateFAQItem = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.deleteFAQItem = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.addFAQSection = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.updateFAQSection = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.deleteFAQSection = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.reorderFAQSection = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
exports.updateSidebarNav = (req, res) =>
res.status(404).json({ error: "Endpoint deprecated" });
+3
View File
@@ -54,10 +54,13 @@ exports.index = async (req, res) => {
data[s] = data[s] || defaults[s];
});
const frontendUrl = process.env.FRONTEND_URL || "";
return res.render("admin/home/index", {
layout: "layouts/main",
title: "Home Management",
data,
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
-539
View File
@@ -1,539 +0,0 @@
const Insurance = require("../models/insurance");
const { addBaseUrlToImages } = require("../utils/imageHelper");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
// API để lấy insurance data (cho frontend)
exports.api = async (req, res) => {
try {
const language = req.query.lang || "en";
// Sử dụng getDefault để đảm bảo luôn có data
const insurance = await Insurance.getDefault(language);
// Trả về data với cấu trúc mới
const insuranceData = insurance.toObject();
// Sử dụng helper để thêm base URL vào đường dẫn ảnh
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(insuranceData, baseUrl);
// Trả về trực tiếp hero, page, content (không wrap trong object)
res.json({
hero: processedData.hero,
page: processedData.page,
content: processedData.content,
});
} catch (error) {
console.error("API Error:", error);
res.status(500).json({
success: false,
error: "Error loading insurance data",
message: error.message,
});
}
};
// API để lấy toàn bộ insurance data (cho admin)
exports.getInsuranceData = async (req, res) => {
try {
const language = req.query.lang || "en";
const insurance = await Insurance.findOne({
name: "default",
language: language,
});
if (!insurance) {
return res.status(404).json({
success: false,
error: "Insurance data not found",
});
}
const insuranceData = insurance.toObject();
// Thêm base URL vào đường dẫn ảnh
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(insuranceData, baseUrl);
res.json({
success: true,
data: processedData,
});
} catch (error) {
console.error("Error getting insurance data:", error);
res.status(500).json({
success: false,
error: "Error loading insurance data",
});
}
};
// API để lấy data theo ngôn ngữ
exports.getByLanguage = async (req, res) => {
try {
const language = req.params.lang || "en";
const insurance = await Insurance.findOne({
name: "default",
language: language,
});
if (!insurance) {
return res.status(404).json({
success: false,
error: "Insurance data not found",
});
}
const insuranceData = insurance.toObject();
// Thêm base URL vào đường dẫn ảnh
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(insuranceData, baseUrl);
res.json({
success: true,
data: {
hero: processedData.hero,
page: processedData.page,
content: processedData.content,
},
});
} catch (error) {
console.error("Error getting insurance by language:", error);
res.status(500).json({
success: false,
error: "Error loading insurance data",
});
}
};
// Render admin view
exports.index = async (req, res) => {
try {
// Luôn đảm bảo có default data
const insurance = await Insurance.getDefault("en");
const data = insurance.toObject();
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
res.render("admin/insurance/index", {
title: "Insurance Management",
layout: "layouts/main",
data,
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error("Error in insurance index:", error);
req.flash("error_msg", "An error occurred while loading the page");
res.redirect("/admin/dashboard");
}
};
// Seed data từ JSON file (cấu trúc mới)
exports.seed = async (req, res) => {
try {
const fs = require("fs").promises;
const path = require("path");
// Đọc file JSON
const jsonPath = path.join(__dirname, "../data/insurance.json");
const jsonData = JSON.parse(await fs.readFile(jsonPath, "utf8"));
console.log("Seeding insurance from JSON...");
// Migrate từ cấu trúc cũ sang mới
const insurance = await Insurance.migrateFromJson(jsonData, "en");
res.json({
success: true,
message: "Insurance data seeded successfully",
data: {
id: insurance._id,
hero: insurance.hero,
page: insurance.page,
content: insurance.content,
},
});
} catch (error) {
console.error("Error seeding insurance:", error);
res.status(500).json({
success: false,
error: error.message || "Error seeding insurance data",
});
}
};
// API preview cho admin (tạo HTML preview)
exports.preview = async (req, res) => {
try {
const { hero, page, content } = req.body;
// Parse JSON strings
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
console.error("JSON parse error:", e);
return null;
}
}
return data;
};
const heroData = parseJson(hero) || {};
const pageData = parseJson(page) || {};
const contentData = parseJson(content) || {};
// Thêm base URL vào đường dẫn ảnh cho preview
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedHeroData = addBaseUrlToImages(heroData, baseUrl);
// Render preview HTML
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${pageData.title || "Insurance Preview"}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { font-family: Arial, sans-serif; }
.hero-section {
background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
url('${processedHeroData.backgroundImage || ""}');
background-size: cover;
background-position: center;
color: white;
padding: 100px 20px;
text-align: center;
}
.page-header {
padding: 40px 20px;
background: #f8f9fa;
}
.content-section {
padding: 40px 20px;
}
.content-item {
margin-bottom: 20px;
}
</style>
</head>
<body>
<!-- Hero Section -->
<div class="hero-section">
<h1>${heroData.title || "Insurance"}</h1>
<p>${heroData.subtitle || ""}</p>
</div>
<!-- Page Header -->
<div class="page-header">
<div class="container">
<h2>${pageData.title || "Insurance Information"}</h2>
${pageData.divider !== false ? "<hr>" : ""}
</div>
</div>
<!-- Content Section -->
<div class="content-section">
<div class="container">
${renderContentItems(contentData.content || [])}
</div>
</div>
</body>
</html>
`;
res.send(html);
} catch (error) {
console.error("Error generating preview:", error);
res.status(500).send("Error generating preview");
}
};
// Helper function để render content items
function renderContentItems(contentItems) {
if (!Array.isArray(contentItems) || contentItems.length === 0) {
return "<p>No content available.</p>";
}
return contentItems
.map((item) => {
switch (item.type) {
case "header":
return `<h${item.level || 2} class="content-item">${item.text}</h${item.level || 2}>`;
case "paragraph":
return `<p class="content-item">${item.text}</p>`;
case "section":
return `
<div class="content-item">
<h3>${item.title}</h3>
<p>${item.content}</p>
</div>
`;
case "list":
const listItems = (item.items || [])
.map((li) => `<li>${li}</li>`)
.join("");
return `<ul class="content-item">${listItems}</ul>`;
case "note":
return `<div class="alert alert-info content-item">${item.text}</div>`;
case "embed":
if (item.source === "youtube") {
return `
<div class="content-item">
<iframe width="${item.width || 560}" height="${item.height || 315}"
src="${item.url || item.embed}"
frameborder="0" allowfullscreen></iframe>
${item.caption ? `<p class="text-muted mt-2">${item.caption}</p>` : ""}
</div>
`;
}
return "";
default:
return "";
}
})
.join("");
}
// API để tạo insurance mới (cho các ngôn ngữ khác)
exports.create = async (req, res) => {
try {
const { hero, page, content, language } = req.body;
if (!language) {
return res.status(400).json({
success: false,
error: "Language is required",
});
}
// Kiểm tra đã tồn tại chưa
const existing = await Insurance.findOne({
name: "default",
language: language,
});
if (existing) {
return res.status(400).json({
success: false,
error: "Insurance already exists for this language",
});
}
// Parse JSON nếu cần
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
console.error("JSON parse error:", e);
return null;
}
}
return data;
};
const insurance = new Insurance({
name: "default",
language: language,
hero: parseJson(hero) || {},
page: parseJson(page) || {},
content: parseJson(content) || {},
version: "2.0.0",
isActive: true,
migratedFromOldStructure: false,
});
await insurance.save();
res.json({
success: true,
message: "Insurance created successfully for language: " + language,
data: insurance,
});
} catch (error) {
console.error("Error creating insurance:", error);
res.status(500).json({
success: false,
error: error.message || "Error creating insurance",
});
}
};
// Cập nhật dữ liệu insurance (CẬP NHẬT CẤU TRÚC MỚI)
exports.update = async (req, res) => {
try {
const { hero, page, content } = req.body;
// Parse JSON strings
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
console.error("JSON parse error:", e);
return null;
}
}
return data;
};
// Parse all data với cấu trúc mới
const heroData = parseJson(hero) || {};
const pageData = parseJson(page) || {};
const contentData = parseJson(content) || {};
// Normalize embed blocks (convert YouTube watch URLs to /embed/ URLs)
function extractYouTubeId(url) {
const regex =
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
const match = url.match(regex);
return match ? match[1] : null;
}
if (contentData && Array.isArray(contentData.content)) {
contentData.content.forEach((item) => {
if (item.type === "embed" && item.source === "youtube") {
if (item.url && item.url.includes("watch?v=")) {
const videoId = extractYouTubeId(item.url);
if (videoId) {
item.url = `https://www.youtube.com/embed/${videoId}`;
item.videoId = videoId;
}
}
if (item.embed && item.embed.includes("watch?v=")) {
const videoId = extractYouTubeId(item.embed);
if (videoId) {
item.embed = `https://www.youtube.com/embed/${videoId}`;
item.videoId = videoId;
}
}
}
});
}
// Tìm hoặc tạo insurance
let insurance = await Insurance.findOne({
name: "default",
language: "en",
});
// ✅ Capture BEFORE state
const beforeData = insurance
? JSON.parse(
JSON.stringify(insurance.toObject ? insurance.toObject() : insurance),
)
: {};
if (!insurance) {
insurance = new Insurance({
name: "default",
language: "en",
hero: heroData,
page: pageData,
content: contentData,
version: "2.0.0",
isActive: true,
});
} else {
insurance.hero = heroData;
insurance.page = pageData;
insurance.content = contentData;
insurance.version = "2.0.0";
}
await insurance.save();
// ✅ Capture AFTER state
const afterData = JSON.parse(
JSON.stringify(insurance.toObject ? insurance.toObject() : insurance),
);
// ✅ AUDIT LOGGING - Insurance Updated
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Insurance",
documentId: insurance._id,
action: AUDIT_ACTIONS.UPDATE_INSURANCE,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Insurance updated successfully");
res.redirect("/admin/insurance");
} catch (err) {
console.error("Error updating insurance:", err);
req.flash("error_msg", err.message || "Error updating insurance");
res.redirect("/admin/insurance");
}
};
// API để xóa insurance (theo ngôn ngữ)
exports.delete = async (req, res) => {
try {
const language = req.params.lang;
if (!language) {
return res.status(400).json({
success: false,
error: "Language parameter is required",
});
}
// Không cho phép xóa tiếng Anh mặc định
if (language === "en") {
return res.status(400).json({
success: false,
error: "Cannot delete default English insurance data",
});
}
const result = await Insurance.deleteOne({
name: "default",
language: language,
});
if (result.deletedCount === 0) {
return res.status(404).json({
success: false,
error: "Insurance not found for this language",
});
}
res.json({
success: true,
message: "Insurance deleted successfully for language: " + language,
});
} catch (error) {
console.error("Error deleting insurance:", error);
res.status(500).json({
success: false,
error: error.message || "Error deleting insurance",
});
}
};
-197
View File
@@ -1,197 +0,0 @@
const Safety = require("../models/safety");
const { addBaseUrlToImages } = require("../utils/imageHelper");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
// Lấy dữ liệu Safety từ MongoDB
const getSafetyData = async () => {
const safety = await Safety.findOne().sort({ updatedAt: -1 });
if (!safety) {
return null;
}
return safety.toObject();
};
// API endpoint cho frontend
exports.api = async (req, res) => {
try {
const safety = await getSafetyData();
if (!safety) {
return res.status(404).json({ error: "Safety data not found" });
}
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(safety, baseUrl);
res.json(processedData);
} catch (err) {
console.error("Safety API error:", err);
res.status(500).json({ error: "Error loading safety data" });
}
};
// Hiển thị danh sách Safety cho admin
exports.index = async (req, res) => {
try {
const items = await Safety.find().sort({ updatedAt: -1 }).limit(10);
// Lấy bản ghi mới nhất hoặc object rỗng nếu chưa có dữ liệu
const latest = items && items.length > 0 ? items[0] : null;
const data = latest
? latest.toObject
? latest.toObject()
: latest
: {
hero: { title: "", banner: "" },
approach: {},
approachImgs: [],
approachStats: [],
approachFeatures: [],
approachCards: [],
philosophy: {},
philosophyCards: [],
security: {},
securityCards: [],
};
res.render("admin/safety/index", {
layout: "layouts/main",
title: "Safety Management",
items,
data,
frontendUrl:
process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"),
currentPath: req.path,
user: req.session.user,
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading Safety data");
res.redirect("/admin/dashboard");
}
};
// Hiển thị form tạo mới Safety
exports.createForm = async (req, res) => {
try {
res.render("admin/safety/create", {
layout: "layouts/main",
title: "Create Safety",
currentPath: req.path,
user: req.session.user,
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading create form");
res.redirect("/admin/safety");
}
};
// Tạo mới Safety
exports.create = async (req, res) => {
try {
const safetyData = req.body; // Tùy chỉnh parse nếu cần
const newSafety = new Safety(safetyData);
await newSafety.save();
req.flash("success_msg", "Safety created successfully");
res.redirect("/admin/safety");
} catch (err) {
console.error("Create error:", err);
req.flash("error_msg", `Create error: ${err.message || "Unknown"}`);
res.redirect("/admin/safety/create");
}
};
// Cập nhật Safety
exports.update = async (req, res) => {
try {
const { hero, approach, philosophy, security } = req.body;
// Parse JSON strings
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
return null;
}
}
return data;
};
const heroData = parseJson(hero);
const approachData = parseJson(approach);
const philosophyData = parseJson(philosophy);
const securityData = parseJson(security);
// Tìm hoặc tạo safety record
const items = await Safety.find().sort({ updatedAt: -1 }).limit(1);
let safety = items && items.length > 0 ? items[0] : null;
// ✅ Capture BEFORE state
const beforeData = safety
? JSON.parse(JSON.stringify(safety.toObject ? safety.toObject() : safety))
: {};
if (!safety) {
// Tạo mới
safety = new Safety({
hero: heroData || { title: "", banner: "" },
approach: approachData || {},
philosophy: philosophyData || {},
security: securityData || {},
});
} else {
// Cập nhật
if (heroData) safety.hero = heroData;
if (approachData) safety.approach = approachData;
if (philosophyData) safety.philosophy = philosophyData;
if (securityData) safety.security = securityData;
}
await safety.save();
// ✅ Capture AFTER state
const afterData = JSON.parse(
JSON.stringify(safety.toObject ? safety.toObject() : safety),
);
// ✅ AUDIT LOGGING - Safety Updated
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Safety",
documentId: safety._id,
action: AUDIT_ACTIONS.UPDATE_SAFETY,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Safety updated successfully");
res.redirect("/admin/safety");
} catch (err) {
console.error("Update error:", err);
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
res.redirect("/admin/safety");
}
};
// Xóa Safety
exports.delete = async (req, res) => {
try {
const safety = await Safety.findById(req.params.id);
if (!safety) {
req.flash("error_msg", "Safety record not found");
return res.redirect("/admin/safety");
}
await Safety.findByIdAndDelete(req.params.id);
req.flash("success_msg", "Safety record deleted successfully");
res.redirect("/admin/safety");
} catch (err) {
console.error("Delete error:", err);
req.flash("error_msg", `Delete error: ${err.message || "Unknown"}`);
res.redirect("/admin/safety");
}
};
-321
View File
@@ -1,321 +0,0 @@
const Header = require("../models/header");
// Get all social links
exports.index = async (req, res) => {
try {
const header = await Header.findOne({ status: "active" }).sort({ order: 1 });
if (!header) {
return res.status(404).json({
success: false,
message: "No active header found",
});
}
res.json({
success: true,
data: header.top?.socialLinks || [],
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
};
// Get single social link by platform
exports.show = async (req, res) => {
try {
const { platform } = req.params;
const header = await Header.findOne({ status: "active" }).sort({ order: 1 });
if (!header) {
return res.status(404).json({
success: false,
message: "No active header found",
});
}
const socialLink = header.top?.socialLinks?.find((link) => link.platform === platform);
if (!socialLink) {
return res.status(404).json({
success: false,
message: "Social link not found",
});
}
res.json({
success: true,
data: socialLink,
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
};
// Create social link
exports.store = async (req, res) => {
try {
let { platform, url, icon } = req.body;
// Convert platform to lowercase
platform = platform.toLowerCase().trim();
url = url.trim();
icon = icon ? icon.trim() : null;
console.log("Creating social link:", { platform, url, icon });
// Validate required fields
if (!platform || !url) {
console.log("Validation failed: platform or url missing");
return res.status(400).json({
success: false,
message: "Platform and URL are required",
});
}
// Validate platform is in enum
const validPlatforms = ["linkedin", "twitter", "instagram", "youtube", "facebook"];
if (!validPlatforms.includes(platform)) {
console.log("Invalid platform:", platform);
return res.status(400).json({
success: false,
message: `Invalid platform. Must be one of: ${validPlatforms.join(", ")}`,
});
}
// Find header
let header = await Header.findOne({ status: "active" }).sort({ order: 1 });
if (!header) {
console.log("No active header found");
return res.status(404).json({
success: false,
message: "No active header found",
});
}
console.log("Found header:", header._id);
// Check if platform already exists
const existingLink = header.top?.socialLinks?.find((link) => link.platform === platform);
if (existingLink) {
console.log("Platform already exists:", platform);
return res.status(400).json({
success: false,
message: `Social link for ${platform} already exists`,
});
}
// Add new social link
if (!header.top) {
header.top = {};
}
if (!header.top.socialLinks) {
header.top.socialLinks = [];
}
// Calculate next order number
const maxOrder =
header.top.socialLinks.length > 0 ? Math.max(...header.top.socialLinks.map((link) => link.order || 0)) : 0;
header.top.socialLinks.push({
platform,
url,
icon: icon || `fa-brands fa-${platform}`,
order: maxOrder + 1,
});
console.log("Saving header with new social link");
await header.save();
console.log("Social link created successfully");
res.status(201).json({
success: true,
message: "Social link created successfully",
data: header.top.socialLinks[header.top.socialLinks.length - 1],
});
} catch (error) {
console.error("Error creating social link:", error);
res.status(400).json({
success: false,
message: error.message,
});
}
};
// Update social link
exports.update = async (req, res) => {
try {
let { platform } = req.params;
let { url, icon } = req.body;
// Convert to lowercase
platform = platform.toLowerCase().trim();
url = url.trim();
icon = icon ? icon.trim() : null;
// Validate required fields
if (!url) {
return res.status(400).json({
success: false,
message: "URL is required",
});
}
// Find header
const header = await Header.findOne({ status: "active" }).sort({ order: 1 });
if (!header) {
return res.status(404).json({
success: false,
message: "No active header found",
});
}
// Find and update social link
const socialLink = header.top?.socialLinks?.find((link) => link.platform === platform);
if (!socialLink) {
return res.status(404).json({
success: false,
message: "Social link not found",
});
}
socialLink.url = url;
if (icon) {
socialLink.icon = icon;
}
await header.save();
res.json({
success: true,
message: "Social link updated successfully",
data: socialLink,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
};
// Delete social link
exports.destroy = async (req, res) => {
try {
let { platform } = req.params;
// Convert to lowercase
platform = platform.toLowerCase().trim();
console.log("Deleting social link:", platform);
// Find header
const header = await Header.findOne({ status: "active" }).sort({ order: 1 });
if (!header) {
console.log("No active header found");
return res.status(404).json({
success: false,
message: "No active header found",
});
}
// Find and remove social link
const index = header.top?.socialLinks?.findIndex((link) => link.platform === platform);
if (index === -1 || index === undefined) {
console.log("Social link not found:", platform);
return res.status(404).json({
success: false,
message: "Social link not found",
});
}
const deletedLink = header.top.socialLinks.splice(index, 1);
console.log("Saving header after delete");
await header.save();
console.log("Social link deleted successfully");
res.json({
success: true,
message: "Social link deleted successfully",
data: deletedLink[0],
});
} catch (error) {
console.error("Error deleting social link:", error);
res.status(500).json({
success: false,
message: error.message,
});
}
};
// Bulk update social links (used for reordering and batch updates)
exports.reorder = async (req, res) => {
try {
const { socialLinks } = req.body;
if (!Array.isArray(socialLinks)) {
return res.status(400).json({
success: false,
message: "socialLinks must be an array",
});
}
// Find header
let header = await Header.findOne({ status: "active" }).sort({ order: 1 });
if (!header) {
return res.status(404).json({
success: false,
message: "No active header found",
});
}
// Validate all social links
for (const link of socialLinks) {
if (!link.platform || !link.url) {
return res.status(400).json({
success: false,
message: "Each social link must have platform and url",
});
}
}
// Update social links with order field
if (!header.top) {
header.top = {};
}
header.top.socialLinks = socialLinks.map((link, index) => ({
platform: link.platform,
url: link.url,
icon: link.icon || `fa-brands fa-${link.platform}`,
order: link.order || index + 1, // Use provided order or calculate from index
}));
await header.save();
res.json({
success: true,
message: "Social links updated successfully",
data: header.top.socialLinks,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
};
-574
View File
@@ -1,574 +0,0 @@
// controllers/termsController.js
const Terms = require("../models/terms");
const { addBaseUrlToImages } = require("../utils/imageHelper"); // Import helper
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
// API để lấy terms data (cho frontend)
exports.api = async (req, res) => {
try {
const language = req.query.lang || "en";
// Sử dụng getDefault để đảm bảo luôn có data
const terms = await Terms.getDefault(language);
// Trả về data với cấu trúc mới
const termsData = terms.toObject();
// Sử dụng helper để thêm base URL vào đường dẫn ảnh
// Truyền baseUrl từ request hoặc từ environment
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(termsData, baseUrl);
res.json({
success: true,
data: {
hero: processedData.hero,
page: processedData.page,
content: processedData.content,
},
});
} catch (error) {
console.error("API Error:", error);
res.status(500).json({
success: false,
error: "Error loading terms data",
message: error.message,
});
}
};
// API để lấy toàn bộ terms data (cho admin)
exports.getTermsData = async (req, res) => {
try {
const language = req.query.lang || "en";
const terms = await Terms.findOne({ name: "default", language: language });
if (!terms) {
return res.status(404).json({
success: false,
error: "Terms data not found",
});
}
const termsData = terms.toObject();
// Thêm base URL vào đường dẫn ảnh
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(termsData, baseUrl);
res.json({
success: true,
data: processedData,
});
} catch (error) {
console.error("Error getting terms data:", error);
res.status(500).json({
success: false,
error: "Error loading terms data",
});
}
};
// API để lấy data theo ngôn ngữ
exports.getByLanguage = async (req, res) => {
try {
const language = req.params.lang || "en";
const terms = await Terms.findOne({ name: "default", language: language });
if (!terms) {
return res.status(404).json({
success: false,
error: "Terms data not found for language: " + language,
});
}
const termsData = terms.toObject();
// Thêm base URL vào đường dẫn ảnh
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(termsData, baseUrl);
res.json({
success: true,
data: {
hero: processedData.hero,
page: processedData.page,
content: processedData.content,
},
});
} catch (error) {
console.error("Error getting terms by language:", error);
res.status(500).json({
success: false,
error: "Error loading terms data",
});
}
};
// Render admin view (không cần thêm baseUrl ở đây vì dùng trong CMS)
exports.index = async (req, res) => {
try {
// Luôn đảm bảo có default data
const terms = await Terms.getDefault("en");
const data = terms.toObject();
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
res.render("admin/terms/index", {
title: "Terms & Conditions Management",
layout: "layouts/main",
data, // Không cần addBaseUrlToImages cho admin view
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error("Error in terms index:", error);
req.flash("error_msg", "An error occurred while loading the page");
res.redirect("/admin/dashboard");
}
};
// Cập nhật dữ liệu terms (CẬP NHẬT CẤU TRÚC MỚI)
exports.update = async (req, res) => {
try {
const { hero, page, content } = req.body;
// Parse JSON strings
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
console.error("JSON parse error:", e);
return null;
}
}
return data;
};
// Parse all data với cấu trúc mới
const heroData = parseJson(hero) || {};
const pageData = parseJson(page) || {};
const contentData = parseJson(content) || {};
// Normalize embed blocks (convert YouTube watch URLs to /embed/ URLs)
function extractYouTubeId(url) {
if (!url || typeof url !== "string") return null;
// common YouTube URL patterns
const m = url.match(
/(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/,
);
return m ? m[1] : null;
}
// Trong exports.update
if (contentData && Array.isArray(contentData.content)) {
contentData.content = contentData.content.map((item) => {
if (item && item.type === "embed") {
let embedUrl = item.embed || item.url || item.source || "";
// Luôn chuyển đổi sang embed URL nếu là watch URL
if (embedUrl.includes("youtube.com/watch")) {
const videoId = extractYouTubeId(embedUrl);
if (videoId) {
item.embed = `https://www.youtube.com/embed/${videoId}`;
item.videoId = videoId;
}
}
// Đảm bảo có videoId
else if (embedUrl && !item.videoId) {
const videoId = extractYouTubeId(embedUrl);
if (videoId) {
item.videoId = videoId;
}
}
}
return item;
});
}
// Tìm hoặc tạo terms
let terms = await Terms.findOne({ name: "default", language: "en" });
// ✅ Capture BEFORE state
const beforeData = terms
? JSON.parse(JSON.stringify(terms.toObject ? terms.toObject() : terms))
: {};
if (!terms) {
// Tạo mới với cấu trúc mới
terms = new Terms({
name: "default",
language: "en",
hero: heroData,
page: pageData,
content: contentData,
version: "2.0.0",
isActive: true,
migratedFromOldStructure: false,
});
} else {
// Update existing với cấu trúc mới
terms.hero = heroData;
terms.page = pageData;
terms.content = contentData;
terms.version = "2.0.0";
terms.migratedFromOldStructure = false;
terms.updatedAt = new Date();
}
await terms.save();
// ✅ Capture AFTER state
const afterData = JSON.parse(
JSON.stringify(terms.toObject ? terms.toObject() : terms),
);
// ✅ AUDIT LOGGING - Terms Updated
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Terms",
documentId: terms._id,
action: AUDIT_ACTIONS.UPDATE_TERMS,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Terms & Conditions updated successfully");
res.redirect("/admin/terms-conditions");
} catch (err) {
console.error("Error updating terms:", err);
req.flash("error_msg", err.message || "Error updating terms");
res.redirect("/admin/terms-conditions");
}
};
// Seed data từ JSON file mới (cấu trúc mới)
exports.seed = async (req, res) => {
try {
const fs = require("fs").promises;
const path = require("path");
// Đọc file JSON
const jsonPath = path.join(__dirname, "../data/terms-conditions.json");
const jsonData = JSON.parse(await fs.readFile(jsonPath, "utf8"));
console.log("Seeding from JSON...");
console.log("JSON structure keys:", Object.keys(jsonData));
// Kiểm tra cấu trúc JSON
let terms;
if (jsonData.hero && jsonData.page && jsonData.content) {
// Cấu trúc mới
console.log("Using new structure (hero, page, content)");
terms = await Terms.migrateFromNewJson(jsonData, "en");
} else if (jsonData.hero && jsonData.termsHeader && jsonData.sections) {
// Cấu trúc cũ
console.log("Using old structure, converting to new...");
terms = await Terms.migrateFromJson(jsonData, "en");
} else {
throw new Error("Unknown JSON structure");
}
res.json({
success: true,
message: "Terms data seeded successfully",
data: {
id: terms._id,
hero: terms.hero,
page: terms.page,
content: terms.content,
},
});
} catch (error) {
console.error("Error seeding terms:", error);
res.status(500).json({
success: false,
error: error.message || "Error seeding terms data",
});
}
};
// API preview cho admin (tạo HTML preview)
exports.preview = async (req, res) => {
try {
const { hero, page, content } = req.body;
// Parse JSON strings
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
console.error("JSON parse error:", e);
return null;
}
}
return data;
};
const heroData = parseJson(hero) || {};
const pageData = parseJson(page) || {};
const contentData = parseJson(content) || {};
// Thêm base URL vào đường dẫn ảnh cho preview
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedHeroData = addBaseUrlToImages(heroData, baseUrl);
// Render preview HTML
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${pageData.title || "Terms & Conditions Preview"}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { font-family: Arial, sans-serif; }
.hero-section {
background: linear-gradient(rgba(0,0,0,0.7), rgba(0,0,0,0.7)),
url('${processedHeroData.backgroundImage || ""}');
background-size: cover;
background-position: center;
color: white;
padding: 100px 20px;
text-align: center;
}
.page-header {
padding: 40px 20px;
background: #f8f9fa;
}
.content-section {
padding: 40px 20px;
}
.content-item {
margin-bottom: 20px;
}
</style>
</head>
<body>
<!-- Hero Section -->
<div class="hero-section">
<h1>${heroData.title || "Terms & Conditions"}</h1>
</div>
<!-- Page Header -->
<div class="page-header">
<div class="container">
<h2>${pageData.title || "Terms & Conditions"}</h2>
${pageData.divider !== false ? "<hr>" : ""}
</div>
</div>
<!-- Content Section -->
<div class="content-section">
<div class="container">
${renderContentItems(contentData.content || [])}
</div>
</div>
</body>
</html>
`;
res.send(html);
} catch (error) {
console.error("Error generating preview:", error);
res.status(500).send("Error generating preview");
}
};
// Helper function để render content items
function renderContentItems(contentItems) {
if (!Array.isArray(contentItems) || contentItems.length === 0) {
return "<p>No content available.</p>";
}
return contentItems
.map((item) => {
switch (item.type) {
case "paragraph":
return `<div class="content-item"><p>${item.text || ""}</p></div>`;
case "section":
let html = `<div class="content-item">`;
html += `<h3>${item.title || ""}</h3>`;
html += `<p>${item.content || ""}</p>`;
if (item.subsections && item.subsections.length > 0) {
item.subsections.forEach((subsection) => {
if (subsection.type === "cancellation_table") {
html += `<h4>${subsection.title || ""}</h4>`;
if (subsection.items && subsection.items.length > 0) {
html += "<ul>";
subsection.items.forEach((listItem) => {
html += `<li>${listItem}</li>`;
});
html += "</ul>";
}
} else if (subsection.type === "cancellation_section") {
html += `<h4>${subsection.title || ""}</h4>`;
if (subsection.items && subsection.items.length > 0) {
html += "<ul>";
subsection.items.forEach((listItem) => {
html += `<li>${listItem}</li>`;
});
html += "</ul>";
}
} else if (subsection.type === "note") {
html += `<div class="alert alert-info">${subsection.text || ""}</div>`;
}
});
}
html += `</div>`;
return html;
case "note":
return `<div class="content-item alert alert-info">${item.text || ""}</div>`;
case "embed":
// Support several embed shapes: { embed }, { url }, { source }, { videoId }
const embedSrc =
item.embed ||
item.url ||
item.source ||
(item.videoId
? `https://www.youtube.com/embed/${item.videoId}`
: "");
if (!embedSrc) return `<div class="content-item">Invalid embed</div>`;
return `<div class="content-item embed-item" style="margin-bottom:20px;">
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;">
<iframe src="${embedSrc}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen loading="lazy" referrerpolicy="no-referrer"></iframe>
</div>
</div>`;
default:
return `<div class="content-item">Unknown content type: ${item.type}</div>`;
}
})
.join("");
}
// API để tạo terms mới (cho các ngôn ngữ khác)
exports.create = async (req, res) => {
try {
const { hero, page, content, language } = req.body;
if (!language) {
return res.status(400).json({
success: false,
error: "Language is required",
});
}
// Kiểm tra đã tồn tại chưa
const existing = await Terms.findOne({
name: "default",
language: language,
});
if (existing) {
return res.status(400).json({
success: false,
error: "Terms already exists for language: " + language,
});
}
// Parse JSON nếu cần
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
console.error("JSON parse error:", e);
return null;
}
}
return data;
};
const terms = new Terms({
name: "default",
language: language,
hero: parseJson(hero) || {},
page: parseJson(page) || {},
content: parseJson(content) || {},
version: "2.0.0",
isActive: true,
migratedFromOldStructure: false,
});
await terms.save();
res.json({
success: true,
message: "Terms created successfully for language: " + language,
data: terms,
});
} catch (error) {
console.error("Error creating terms:", error);
res.status(500).json({
success: false,
error: error.message || "Error creating terms",
});
}
};
// API để xóa terms (theo ngôn ngữ)
exports.delete = async (req, res) => {
try {
const language = req.params.lang;
if (!language) {
return res.status(400).json({
success: false,
error: "Language is required",
});
}
// Không cho phép xóa tiếng Anh mặc định
if (language === "en") {
return res.status(400).json({
success: false,
error: "Cannot delete default English terms",
});
}
const result = await Terms.deleteOne({
name: "default",
language: language,
});
if (result.deletedCount === 0) {
return res.status(404).json({
success: false,
error: "Terms not found for language: " + language,
});
}
res.json({
success: true,
message: "Terms deleted successfully for language: " + language,
});
} catch (error) {
console.error("Error deleting terms:", error);
res.status(500).json({
success: false,
error: error.message || "Error deleting terms",
});
}
};
-138
View File
@@ -1,138 +0,0 @@
const { addBaseUrlToImages } = require("../utils/imageHelper");
const Home = require("../models/home");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
// Get testimonial data from Home model
const getTestimonialData = async () => {
const home = await Home.findOne().sort({ updatedAt: -1 });
if (!home || !home.testimonials) {
return null;
}
return home.testimonials.toObject
? home.testimonials.toObject()
: home.testimonials;
};
// API to get testimonial data
exports.api = async (req, res) => {
try {
const testimonial = await getTestimonialData();
if (!testimonial) {
return res.status(404).json({ error: "Testimonial data not found" });
}
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(testimonial, baseUrl);
res.json(processedData);
} catch (err) {
console.error("API Error:", err);
res.status(500).json({ error: "Error loading testimonial data" });
}
};
// Render admin view
exports.index = async (req, res) => {
try {
const data = (await getTestimonialData()) || {
heading: "Student Reviews & Testimonials",
subheading: "What Our Students Say",
videoUrl: "",
videoThumbnail: "",
items: [],
};
const frontendUrl = process.env.FRONTEND_URL;
res.render("admin/home/testimonial/index", {
title: "Testimonials Management",
layout: "layouts/main",
data,
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error("Error in testimonial index:", error);
req.flash("error_msg", "An error occurred while loading the page");
res.redirect("/admin/dashboard");
}
};
// Cập nhật dữ liệu testimonial (chỉ update phần testimonials của Home)
exports.update = async (req, res) => {
try {
const { heading, subheading, videoUrl, videoThumbnail, items } = req.body;
// Parse JSON strings nếu cần
const parseJson = (data) => {
if (!data) return null;
if (typeof data === "string") {
try {
return JSON.parse(data);
} catch (e) {
return null;
}
}
return data;
};
const itemsData = parseJson(items);
// Tìm hoặc tạo Home document
let home = await Home.findOne().sort({ updatedAt: -1 });
if (!home) {
home = new Home({});
}
// ✅ Capture BEFORE state
const beforeData = home.testimonials
? JSON.parse(
JSON.stringify(
home.testimonials.toObject
? home.testimonials.toObject()
: home.testimonials,
),
)
: {};
const updatedTestimonialData = {
heading: heading || "Student Reviews & Testimonials",
subheading: subheading || "What Our Students Say",
videoUrl: videoUrl || "",
videoThumbnail: videoThumbnail || "",
items: itemsData || [],
};
// Cập nhật chỉ phần testimonials
home.testimonials = updatedTestimonialData;
await home.save();
// ✅ Capture AFTER state
const afterData = JSON.parse(JSON.stringify(updatedTestimonialData));
// ✅ AUDIT LOGGING - Testimonial Updated
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Home",
documentId: home._id,
action: AUDIT_ACTIONS.UPDATE_TESTIMONIAL,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Testimonials updated successfully");
res.redirect("/admin/home/testimonials");
} catch (err) {
console.error("Error updating testimonials:", err);
req.flash("error_msg", err.message || "Error updating testimonials");
res.redirect("/admin/home/testimonials");
}
};
-119
View File
@@ -1,119 +0,0 @@
const { addBaseUrlToImages } = require("../utils/imageHelper");
const Home = require("../models/home");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
// Get videoGallery data from Home model
const getVideoGalleryData = async () => {
const home = await Home.findOne().sort({ updatedAt: -1 });
if (!home || !home.videoGallery) {
return null;
}
return home.videoGallery.toObject
? home.videoGallery.toObject()
: home.videoGallery;
};
// API to get videoGallery data
exports.api = async (req, res) => {
try {
const videoGallery = await getVideoGalleryData();
if (!videoGallery) {
return res.status(404).json({ error: "Video Gallery data not found" });
}
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(videoGallery, baseUrl);
res.json(processedData);
} catch (err) {
console.error("API Error:", err);
res.status(500).json({ error: "Error loading video gallery data" });
}
};
// Render admin view
exports.index = async (req, res) => {
try {
const data = (await getVideoGalleryData()) || {
heading: "",
videoUrl: "",
thumbnail: "",
};
const frontendUrl = process.env.FRONTEND_URL;
res.render("admin/home/videoGallery/index", {
title: "Video Gallery Management",
layout: "layouts/main",
data,
frontendUrl,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error("Error in videoGallery index:", error);
req.flash("error_msg", "An error occurred while loading the page");
res.redirect("/admin/dashboard");
}
};
// Cập nhật dữ liệu videoGallery
exports.update = async (req, res) => {
try {
const { heading, videoUrl, thumbnail } = req.body;
// Tìm hoặc tạo Home document
let home = await Home.findOne().sort({ updatedAt: -1 });
if (!home) {
home = new Home({});
}
// ✅ Capture BEFORE state
const beforeData = home.videoGallery
? JSON.parse(
JSON.stringify(
home.videoGallery.toObject
? home.videoGallery.toObject()
: home.videoGallery,
),
)
: {};
const updatedVideoGalleryData = {
heading: heading || "",
videoUrl: videoUrl || "",
thumbnail: thumbnail || "",
};
// Cập nhật chỉ phần videoGallery
home.videoGallery = updatedVideoGalleryData;
await home.save();
// ✅ Capture AFTER state
const afterData = JSON.parse(JSON.stringify(updatedVideoGalleryData));
// ✅ AUDIT LOGGING - Video Gallery Updated
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Home",
documentId: home._id,
action: AUDIT_ACTIONS.UPDATE_VIDEO_GALLERY,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Video Gallery updated successfully");
res.redirect("/admin/home/video-gallery");
} catch (err) {
console.error("Error updating video gallery:", err);
req.flash("error_msg", err.message || "Error updating video gallery");
res.redirect("/admin/home/video-gallery");
}
};