// controllers/visaController.js const addBaseUrlToImages = (data, baseUrl) => { if (!data) return data; // Nếu là mảng, duyệt từng phần tử if (Array.isArray(data)) { return data.map((item) => addBaseUrlToImages(item, baseUrl)); } // Nếu là object, duyệt từng key if (typeof data === "object") { const newObj = {}; for (const [key, value] of Object.entries(data)) { // Kiểm tra nếu key là các trường chứa ảnh và value là string const imageKeys = ["icon", "mainImage", "bannerImage", "image"]; if ( imageKeys.includes(key) && typeof value === "string" && !value.startsWith("http") ) { newObj[key] = `${baseUrl}/${value}` .replace(/\/+/g, "/") .replace(":/", "://"); } // Xử lý riêng cho mảng gallery (mảng các chuỗi) else if (key === "gallery" && Array.isArray(value)) { newObj[key] = value.map((img) => img.startsWith("http") ? img : `${baseUrl}/${img}`.replace(/\/+/g, "/").replace(":/", "://"), ); } // Nếu là object hoặc mảng con khác, đệ quy tiếp else if (typeof value === "object" && value !== null) { newObj[key] = addBaseUrlToImages(value, baseUrl); } else { newObj[key] = value; } } return newObj; } return data; }; const Visa = require("../models/visa"); const slugify = require("slugify"); const writeAuditLog = require("../audit/writeAuditLog"); const diffObject = require("../audit/diffObject"); const AUDIT_ACTIONS = require("../constants/auditAction"); const createSlug = (text) => { return slugify(text, { lower: true, strict: true, locale: "en", trim: true, }); }; // -------------------- Helper Functions -------------------- // Get visa data from MongoDB const getVisaData = async () => { const visa = await Visa.findOne().sort({ updatedAt: -1 }); return visa || {}; }; // Get default visa data structure (updated to match new JSON) const getDefaultVisaData = () => ({ hero: { title: "Visa Service", summaryList: [], }, }); // Helper function: Generate next country ID const getNextCountryId = (countries) => { if (!Array.isArray(countries) || countries.length === 0) return 1; return Math.max(...countries.map((c) => c.id || 0)) + 1; }; // -------------------- Admin Exports -------------------- // Display visa management page exports.index = async (req, res) => { try { // Fetch Visa data let data = await getVisaData(); // If no data exists, use default if (!data || Object.keys(data).length === 0) { data = getDefaultVisaData(); } else { // Merge with defaults to ensure all fields exist const defaultData = getDefaultVisaData(); // Ensure hero section exists with defaults data.hero = data.hero || defaultData.hero; data.hero.title = data.hero.title || "Visa Service"; data.hero.summaryList = data.hero.summaryList || []; } const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; res.render("admin/visa/index", { layout: "layouts/main", title: "Visa Management", data, frontendUrl, currentPath: req.path, user: req.session.user, }); // return res.json(data); } catch (err) { console.error("Visa index error:", err); req.flash("error_msg", "Error loading visa data"); res.redirect("/admin/dashboard"); } }; // Get single country for edit exports.getCountry = async (req, res) => { console.log("--------------------------------------------------"); console.log("🚀 [GET] Request nhận được tại /visa/edit/:id"); try { const { id } = req.params; console.log("📍 ID từ Params (URL):", id, "| Kiểu dữ liệu:", typeof id); const visaData = await getVisaData(); // Kiểm tra cấu trúc dữ liệu tổng if (!visaData) { console.error("❌ Lỗi: Hàm getVisaData() trả về null/undefined"); return res.status(404).json({ error: "Dữ liệu gốc không tồn tại" }); } if (!visaData.hero || !visaData.hero.summaryList) { console.error("❌ Lỗi: Cấu trúc visaData.hero.summaryList không hợp lệ"); return res .status(404) .json({ error: "Không tìm thấy danh sách quốc gia" }); } console.log( "📊 Tổng số quốc gia hiện có trong mảng:", visaData.hero.summaryList.length, ); // 2. Tìm quốc gia theo ID const targetId = parseInt(id); console.log("🔍 Đang tìm kiếm Quốc gia có ID (sau khi parse):", targetId); const country = visaData.hero.summaryList.find((c) => { // Log từng phần tử để kiểm tra kiểu dữ liệu của c.id trong DB // console.log(`Checking country: ${c.name} | ID in DB: ${c.id} (${typeof c.id})`); return c.id === targetId; }); if (!country) { console.warn(`⚠️ Không tìm thấy quốc gia nào khớp với ID: ${targetId}`); // In ra danh sách ID hiện có để so sánh const existingIds = visaData.hero.summaryList.map((c) => c.id); console.log("🆔 Các ID hiện có trong Database:", existingIds); return res.status(404).json({ success: false, error: `Không tìm thấy quốc gia có ID: ${id}`, }); } console.log("✅ Tìm thấy dữ liệu quốc gia:", country.name); // 3. Trả về dữ liệu res.json({ success: true, country: country, }); } catch (err) { console.error("🔴 Lỗi nghiêm trọng tại getCountry Controller:", err); res.status(500).json({ error: "Lỗi hệ thống khi tải thông tin quốc gia" }); } }; // Update visa data (hero title only) exports.update = async (req, res) => { try { // Get current data const currentData = await getVisaData(); // ✅ Capture BEFORE state const beforeData = currentData ? JSON.parse( JSON.stringify( currentData.toObject ? currentData.toObject() : currentData, ), ) : {}; // Create updated data object const updatedData = { ...(currentData.toObject ? currentData.toObject() : currentData), }; // Ensure hero structure exists updatedData.hero = updatedData.hero || { title: "Visa Service", summaryList: [], }; // Update hero title if (req.body.heroTitle) { updatedData.hero.title = req.body.heroTitle; } // Update or create document try { let savedData; if (currentData._id) { savedData = await Visa.findByIdAndUpdate(currentData._id, updatedData, { new: true, }); } else { savedData = await Visa.create(updatedData); } // ✅ Capture AFTER state const afterData = JSON.parse(JSON.stringify(savedData.toObject())); // ✅ AUDIT LOGGING - Visa Updated const changes = diffObject(beforeData, afterData); if (changes.length > 0) { await writeAuditLog({ model: "Visa", documentId: savedData._id, action: AUDIT_ACTIONS.UPDATE_VISA, before: beforeData, after: afterData, changes, req, }); console.log( `✅ Audit log created for Visa update: ${changes.length} changes`, ); } else { console.log("ℹ️ No changes detected for Visa update"); } req.flash("success_msg", "Visa data updated successfully"); return req.session.save(() => res.redirect("/admin/visa")); } catch (dbError) { console.error("Database error:", dbError); req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`); return req.session.save(() => res.redirect("/admin/visa")); } } catch (err) { console.error("Update error:", err); req.flash("error_msg", `Update error: ${err.message || "Unknown"}`); return req.session.save(() => res.redirect("/admin/visa")); } }; // Add new country exports.addCountry = async (req, res) => { try { let visaData = await getVisaData(); // Initialize hero structure if not exist if (!visaData.hero || !visaData.hero.summaryList) { visaData = getDefaultVisaData(); } // ✅ Capture BEFORE state const beforeData = JSON.parse( JSON.stringify(visaData.toObject ? visaData.toObject() : visaData), ); // Validate required fields if (!req.body.name) { return res.status(400).json({ error: "Name is required" }); } const finalSlug = req.body.slug ? createSlug(req.body.slug) : createSlug(req.body.name); // Parse services array let services = []; if (req.body.services) { if (typeof req.body.services === "string") { try { services = JSON.parse(req.body.services); } catch (e) { services = [req.body.services]; } } else if (Array.isArray(req.body.services)) { services = req.body.services; } } // Parse detailedView if provided (optional) let detailedView = null; if (req.body.detailedView) { try { detailedView = typeof req.body.detailedView === "string" ? JSON.parse(req.body.detailedView) : req.body.detailedView; } catch (e) { console.warn("Could not parse detailedView, creating without it"); } } // Create new country object const newCountry = { id: req.body.id || getNextCountryId(visaData.hero.summaryList), name: req.body.name, slug: finalSlug, icon: req.body.icon || "", services: services, ...(detailedView && { detailedView }), }; // Add new country to summaryList visaData.hero.summaryList.push(newCountry); // Update database const updatedData = { ...(visaData.toObject ? visaData.toObject() : visaData), }; let savedData; if (visaData._id) { savedData = await Visa.findByIdAndUpdate(visaData._id, updatedData, { new: true, }); } else { savedData = await Visa.create(updatedData); } // ✅ Capture AFTER state const afterData = JSON.parse( JSON.stringify(savedData.toObject ? savedData.toObject() : savedData), ); // ✅ AUDIT LOGGING - Visa Country Added const changes = diffObject(beforeData, afterData); if (changes.length > 0) { await writeAuditLog({ model: "Visa", documentId: savedData._id, action: AUDIT_ACTIONS.UPDATE_VISA, before: beforeData, after: afterData, changes, req, }); console.log( `✅ Audit log created for Visa country addition: ${changes.length} changes`, ); } console.log(`✅ Country "${newCountry.name}" added successfully`); res.json({ success: true, message: `Country "${newCountry.name}" added successfully`, country: newCountry, }); } catch (err) { console.error("Add country error:", err); res.status(500).json({ error: err.message }); } }; // Update single country exports.updateCountry = async (req, res) => { try { // 1. Lấy ID từ params (URL) const { id } = req.params; let visaData = await getVisaData(); if (!visaData || !visaData.hero || !visaData.hero.summaryList) { return res .status(400) .json({ error: "Cấu trúc dữ liệu Visa không hợp lệ" }); } // ✅ Capture BEFORE state const beforeData = JSON.parse( JSON.stringify(visaData.toObject ? visaData.toObject() : visaData), ); // 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác) const countryIndex = visaData.hero.summaryList.findIndex( (c) => c.id === parseInt(id), ); if (countryIndex === -1) { return res .status(404) .json({ error: `Không tìm thấy quốc gia có ID: ${id}` }); } const currentCountry = visaData.hero.summaryList[countryIndex]; let finalSlug = currentCountry.slug; if (req.body.name) { // Nếu name thay đổi, ta có thể cập nhật lại slug (tùy nhu cầu SEO) // Ở đây ưu tiên: Nếu có slug mới truyền lên thì dùng, không thì tạo từ name mới finalSlug = req.body.slug ? createSlug(req.body.slug) : createSlug(req.body.name); } // 3. Xử lý dữ liệu từ req.body // Vì Client đã gửi JSON stringify, ta lấy trực tiếp hoặc parse nếu cần let services = req.body.services; if (typeof services === "string") { try { services = JSON.parse(services); } catch (e) { services = [services]; } } let detailedView = req.body.detailedView; if (typeof detailedView === "string") { try { detailedView = JSON.parse(detailedView); } catch (e) { detailedView = currentCountry.detailedView; } } // 4. Cập nhật Object quốc gia const updatedCountry = { ...currentCountry, // Giữ các trường cũ id: parseInt(id), // Đảm bảo ID không đổi name: req.body.name || currentCountry.name, slug: finalSlug, icon: req.body.icon || currentCountry.icon, services: Array.isArray(services) ? services : currentCountry.services, detailedView: detailedView || currentCountry.detailedView, }; // 5. Cập nhật vào mảng chính visaData.hero.summaryList[countryIndex] = updatedCountry; // 6. Lưu vào Database if (visaData.markModified) { // Bắt buộc với Mongoose khi thay đổi nội dung bên trong Array/Object visaData.markModified("hero.summaryList"); } let savedData; if (visaData._id) { savedData = await visaData.save(); // Sử dụng save() trực tiếp nếu visaData là Mongoose Document } else { savedData = await Visa.create(visaData); } // ✅ Capture AFTER state const afterData = JSON.parse( JSON.stringify(savedData.toObject ? savedData.toObject() : savedData), ); // ✅ AUDIT LOGGING - Visa Country Updated const changes = diffObject(beforeData, afterData); if (changes.length > 0) { await writeAuditLog({ model: "Visa", documentId: savedData._id, action: AUDIT_ACTIONS.UPDATE_VISA, before: beforeData, after: afterData, changes, req, }); console.log( `✅ Audit log created for Visa country update: ${changes.length} changes`, ); } console.log( `✅ Country "${updatedCountry.name}" updated successfully by ID: ${id}`, ); res.json({ success: true, message: `Quốc gia "${updatedCountry.name}" đã được cập nhật thành công`, country: updatedCountry, }); } catch (err) { console.error("Update country error:", err); res.status(500).json({ error: err.message }); } }; // Delete country exports.deleteCountry = async (req, res) => { try { // 1. Lấy id từ params const { id } = req.params; let visaData = await getVisaData(); if (!visaData || !visaData.hero || !visaData.hero.summaryList) { return res .status(400) .json({ success: false, error: "Cấu trúc dữ liệu Visa không hợp lệ" }); } // 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác) const countryIndex = visaData.hero.summaryList.findIndex( (c) => c.id === parseInt(id), ); if (countryIndex === -1) { return res.status(404).json({ success: false, error: `Không tìm thấy quốc gia có ID: ${id}`, }); } // 3. Xóa phần tử khỏi mảng const deletedCountry = visaData.hero.summaryList[countryIndex]; visaData.hero.summaryList.splice(countryIndex, 1); // 4. Cập nhật vào Database if (visaData.markModified) { visaData.markModified("hero.summaryList"); } if (visaData._id) { await visaData.save(); } else { await Visa.create(visaData); } console.log(`✅ Deleted Successfully: "${deletedCountry.name}"`); return res.json({ success: true, message: `Country "${deletedCountry.name}" Deleted Successfully`, }); } catch (err) { console.error("❌ Error Delete:", err); return res.status(500).json({ success: false, error: err.message }); } }; // -------------------- Public API Exports -------------------- // API to get all visa data for frontend exports.api = async (req, res) => { try { const visaData = await getVisaData(); if (!visaData) { return res.status(404).json({ success: false, error: "Visa data not found", data: null, }); } const heroData = visaData?.hero; // 2. Lấy riêng phần hero (Dùng biến mới, không gán đè vào const) const processedData = heroData; return res.json({ success: true, hero: processedData, }); } catch (err) { console.error("Visa API error:", err); res.status(500).json({ success: false, error: "Error loading visa data", }); } }; // API to get all countries (summaryList only) exports.apiCountries = async (req, res) => { try { const visaData = await getVisaData(); if (!visaData || !visaData.hero || !visaData.hero.summaryList) { return res.status(404).json({ success: false, error: "Countries data not found", data: null, }); } // 1. Lọc bỏ 'viewDetail' khỏi từng quốc gia trong danh sách const filteredCountries = visaData.hero.summaryList.map((item) => { // Tách detailedView ra, gom phần còn lại vào countryInfo const { detailedView, ...countryInfo } = item; return { ...countryInfo, // Lấy mainImage từ sâu bên trong detailedView và gán vào key mới mainImage: detailedView?.activeCountry?.mainImage || "", }; }); // 2. Gắn baseUrl vào ảnh cho danh sách đã lọc const processedData = filteredCountries; return res.json({ success: true, data: processedData, // Lúc này data chỉ chứa thông tin quốc gia, không có viewDetail }); } catch (err) { console.error("Countries API error:", err); res.status(500).json({ success: false, error: "Error loading countries data", }); } }; // API to get single country by slug exports.apiCountry = async (req, res) => { try { const { slug } = req.params; const visaData = await getVisaData(); if (!visaData || !visaData.hero || !visaData.hero.summaryList) { return res.status(404).json({ success: false, error: "Visa data not found", data: null, }); } // 1. Tìm quốc gia khớp với slug const country = visaData.hero.summaryList.find((c) => c.slug === slug); // 2. Kiểm tra nếu không thấy quốc gia hoặc quốc gia đó không có viewDetail if (!country || !country.viewDetail) { return res.status(404).json({ success: false, error: `Detailed information for country "${slug}" not found`, data: null, }); } // 3. Chỉ lấy phần chi tiết (detailed view) // Lưu ý: Chúng ta copy ra object mới để tránh tham chiếu dữ liệu gốc const detailedData = JSON.parse(JSON.stringify(country.viewDetail)); // 4. Gắn baseUrl vào các ảnh nằm trong phần chi tiết này const processedData = detailedData; return res.json({ success: true, data: processedData, }); } catch (err) { console.error("Visa country API error:", err); res.status(500).json({ success: false, error: "Error loading country detailed data", }); } }; // API to get hero data (title + summaryList) exports.apiHero = async (req, res) => { try { const visaData = await getVisaData(); // 1. Kiểm tra dữ liệu gốc if (!visaData || !visaData.hero) { return res.status(404).json({ success: false, error: "Hero data not found", data: null, }); } const { summaryList, ...heroData } = JSON.parse( JSON.stringify(visaData.hero), ); const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; const processedData = addBaseUrlToImages(heroData, baseUrl); return res.json({ success: true, data: processedData, }); } catch (err) { console.error("Visa hero API error:", err); res.status(500).json({ success: false, error: "Error loading hero data", }); } };