Files
cms.uldp.edu.vn/controllers/visaController.js
2026-02-04 13:57:23 +07:00

610 lines
19 KiB
JavaScript

// 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 createSlug = (text) => {
return slugify(text, {
lower: true, // Chuyển về chữ thường
strict: true, // Loại bỏ ký tự đặc biệt
locale: "vi", // Xử lý tiếng Việt chuẩn
trim: true,
});
};
// -------------------- 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) => {
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();
// 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) {
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);
}
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ệ" });
// }
// 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");
}
if (visaData._id) {
await visaData.save(); // Sử dụng save() trực tiếp nếu visaData là Mongoose Document
} else {
await Visa.create(visaData);
}
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 {
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) => {
// Trả về bản sao của item để giữ nguyên cấu trúc gốc bao gồm cả detailedView
return { ...item };
});
// 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",
});
}
};