forked from UKSOURCE/cms.hailearning.edu.vn
UI Visa-VisaDetail
This commit is contained in:
557
controllers/visaController.js
Normal file
557
controllers/visaController.js
Normal file
@@ -0,0 +1,557 @@
|
||||
// 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",
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user