// 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"); // -------------------- Helper Functions -------------------- // Get visa data from MongoDB const getVisaData = async () => { const visa = await Visa.findOne().sort({ updatedAt: -1 }).lean(); 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) => { try { const { slug } = req.params; const visaData = await getVisaData(); if (!visaData.hero || !visaData.hero.summaryList) { return res.status(404).json({ error: "Visa data not found" }); } const country = visaData.hero.summaryList.find((c) => c.slug === slug); if (!country) { return res.status(404).json({ error: `Country "${slug}" not found` }); } res.json(country); } catch (err) { console.error("Get country error:", err); res.status(500).json({ error: "Error loading country" }); } }; // Update visa data (hero title only) exports.update = async (req, res) => { try { // Get current data const currentData = await getVisaData(); // 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; } // Check if there are changes const hasChanges = JSON.stringify(updatedData) !== JSON.stringify(currentData); if (!hasChanges) { req.flash("info_msg", "No changes were made"); return req.session.save(() => res.redirect("/admin/visa")); } // Update or create document try { if (currentData._id) { await Visa.findByIdAndUpdate(currentData._id, updatedData, { new: true, }); } else { await Visa.create(updatedData); } 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(); } // Validate required fields if (!req.body.name || !req.body.slug) { return res.status(400).json({ error: "Name and slug are required" }); } // Check if slug already exists const slugExists = visaData.hero.summaryList.some( (c) => c.slug === req.body.slug, ); if (slugExists) { return res .status(400) .json({ error: `Slug "${req.body.slug}" already exists` }); } // 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: req.body.slug, 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); } 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 { const { slug } = req.params; let visaData = await getVisaData(); if (!visaData.hero || !visaData.hero.summaryList) { return res.status(400).json({ error: "Invalid visa data structure" }); } const countryIndex = visaData.hero.summaryList.findIndex( (c) => c.slug === slug, ); if (countryIndex === -1) { return res.status(404).json({ error: `Country "${slug}" not found` }); } const currentCountry = visaData.hero.summaryList[countryIndex]; // Parse services array let services = currentCountry.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 = currentCountry.detailedView; 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"); } } // Update country data const updatedCountry = { ...currentCountry, name: req.body.name || currentCountry.name, slug: req.body.slug || currentCountry.slug, // Allow slug update icon: req.body.icon || currentCountry.icon, services: services, ...(detailedView && { detailedView }), }; visaData.hero.summaryList[countryIndex] = updatedCountry; // Update database const updatedData = { ...(visaData.toObject ? visaData.toObject() : visaData), }; if (visaData._id) { await Visa.findByIdAndUpdate(visaData._id, updatedData, { new: true }); } else { await Visa.create(updatedData); } console.log(`✅ Country "${updatedCountry.name}" updated successfully`); res.json({ success: true, message: `Country "${updatedCountry.name}" updated successfully`, 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 { const { slug } = req.params; let visaData = await getVisaData(); if (!visaData.hero || !visaData.hero.summaryList) { return res.status(400).json({ error: "Invalid visa data structure" }); } const countryIndex = visaData.hero.summaryList.findIndex( (c) => c.slug === slug, ); if (countryIndex === -1) { return res.status(404).json({ error: `Country "${slug}" not found` }); } const deletedCountry = visaData.hero.summaryList[countryIndex]; visaData.hero.summaryList.splice(countryIndex, 1); // Update database const updatedData = { ...(visaData.toObject ? visaData.toObject() : visaData), }; if (visaData._id) { await Visa.findByIdAndUpdate(visaData._id, updatedData, { new: true }); } else { await Visa.create(updatedData); } console.log(`✅ Country "${deletedCountry.name}" deleted successfully`); res.json({ success: true, message: `Country "${deletedCountry.name}" deleted successfully`, }); } catch (err) { console.error("Delete country error:", err); res.status(500).json({ 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 baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; const processedData = addBaseUrlToImages(heroData, baseUrl); 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, }); } const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; // 1. Lọc bỏ 'viewDetail' khỏi từng quốc gia trong danh sách const filteredCountries = visaData.hero.summaryList.map((item) => { // Dùng destructuring để tách viewDetail ra và gom phần còn lại vào countryInfo const { detailedView, ...countryInfo } = item; return countryInfo; }); // 2. Gắn baseUrl vào ảnh cho danh sách đã lọc const processedData = addBaseUrlToImages(filteredCountries, baseUrl); 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, }); } const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; // 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 = addBaseUrlToImages(detailedData, baseUrl); return res.json({ success: true, data: processedData, // Trả về nội dung của detailedView }); } 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", }); } };