forked from UKSOURCE/cms.lams
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:
@@ -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
@@ -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" });
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user