// 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 = `
No content available.
"; } return contentItems .map((item) => { switch (item.type) { case "paragraph": return `${item.text || ""}
${item.content || ""}
`; if (item.subsections && item.subsections.length > 0) { item.subsections.forEach((subsection) => { if (subsection.type === "cancellation_table") { html += `