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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
300
data/visa.json
Normal file
300
data/visa.json
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
{
|
||||||
|
"hero": {
|
||||||
|
"title": "Visa Service",
|
||||||
|
"summaryList": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "France",
|
||||||
|
"slug": "france",
|
||||||
|
"icon": "/img/home-2/visa/03.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
],
|
||||||
|
"detailedView": {
|
||||||
|
"activeCountry": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "United States of America ",
|
||||||
|
"title": "COUNTRY USA",
|
||||||
|
"mainImage": "/img/inner-page/country-details/details-1.jpg",
|
||||||
|
"description": "The United States is one of the most popular destinations for international students and immigrants, offering world-class universities, diverse cultural experiences, and countless career opportunities...",
|
||||||
|
"additionalInfo": "Our consultancy provides complete guidance for study visas, work permits, and permanent residency pathways tailored to your goals.",
|
||||||
|
"tagline": "Over the last 35 Years we made an impact that is strong & we have long way to go.",
|
||||||
|
"visaTypes": [
|
||||||
|
{
|
||||||
|
"category": "Tourist & Work",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Tourist Visa",
|
||||||
|
"description": "Broad term that can refer to various aspects of interconnectedness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Work Permit",
|
||||||
|
"description": "Broad term that can refer to various aspects of interconnectedness"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Student & Family",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Student",
|
||||||
|
"description": "Broad term that can refer to various aspects of interconnectedness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tourist Visa",
|
||||||
|
"description": "Broad term that can refer to various aspects of interconnectedness"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"visaProcess": {
|
||||||
|
"title": "USA Visa Process",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"number": "01",
|
||||||
|
"title": "Consultation & Eligibility Check",
|
||||||
|
"description": "Our experts review your profile and visa requirements."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "02",
|
||||||
|
"title": "Application Preparation",
|
||||||
|
"description": "We help with document collection, form filling, and statement drafting."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "03",
|
||||||
|
"title": "Submission",
|
||||||
|
"description": "Visa application is submitted online with required fees."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "04",
|
||||||
|
"title": "Interview Guidance",
|
||||||
|
"description": "Get training and mock sessions for embassy interview."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "05",
|
||||||
|
"title": "Approval & Travel",
|
||||||
|
"description": "Once approved, we provide travel and pre-departure guidance."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gallery": [
|
||||||
|
"/img/inner-page/country-details/details-2.jpg",
|
||||||
|
"/img/inner-page/country-details/details-3.png"
|
||||||
|
],
|
||||||
|
"visaCategories": {
|
||||||
|
"title": "Types of USA Visas",
|
||||||
|
"steps": [
|
||||||
|
[
|
||||||
|
"Student Visa (F1, M1, J1)",
|
||||||
|
"Work Visa (H1B, L1)",
|
||||||
|
"Tourist Visa (B1/B2)"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Family/Spouse Visa (K1, IR1, F2A)",
|
||||||
|
"Green Card / Immigrant Visa"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"visaService": {
|
||||||
|
"title": "Our USA Visa Service Options",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"number": "01",
|
||||||
|
"title": "Consultation & Eligibility Check",
|
||||||
|
"description": "Our experts review your profile and visa requirements."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "02",
|
||||||
|
"title": "Application Preparation",
|
||||||
|
"description": "We help with document collection, form filling, and statement drafting."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "03",
|
||||||
|
"title": "Submission",
|
||||||
|
"description": "Visa application is submitted online with required fees."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "04",
|
||||||
|
"title": "Interview Guidance",
|
||||||
|
"description": "Get training and mock sessions for embassy interview."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": "05",
|
||||||
|
"title": "Approval & Travel",
|
||||||
|
"description": "Once approved, we provide travel and pre-departure guidance."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"relatedCountries": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Canada",
|
||||||
|
"icon": "/img/inner-page/country-details/01.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "USA",
|
||||||
|
"icon": "/img/inner-page/country-details/02.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "USA",
|
||||||
|
"icon": "/img/inner-page/country-details/03.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Saint Helena",
|
||||||
|
"icon": "/img/inner-page/country-details/05.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Iran",
|
||||||
|
"icon": "/img/inner-page/country-details/06.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Spain",
|
||||||
|
"icon": "/img/inner-page/country-details/07.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"name": "Japan",
|
||||||
|
"icon": "/img/inner-page/country-details/08.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contactInfo": {
|
||||||
|
"img": "/img/inner-page/country-details/bg.jpg",
|
||||||
|
"sectionTitle": "Visa & Immigration",
|
||||||
|
"helpText": "Need Help? Book Lab Visit",
|
||||||
|
"phone": {
|
||||||
|
"label": "Call Us",
|
||||||
|
"value": "+009 438 222 9540",
|
||||||
|
"link": "tel:+0094382229540"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"label": "Mail Us",
|
||||||
|
"value": "infor@xridergamil.com",
|
||||||
|
"link": "mailto:infor@xridergamil.com"
|
||||||
|
},
|
||||||
|
"location": {
|
||||||
|
"label": "Location",
|
||||||
|
"address": "Toronto, Montreal, City 2026"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "UK",
|
||||||
|
"slug": "uk",
|
||||||
|
"icon": "/img/home-2/visa/11.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Canada",
|
||||||
|
"slug": "canada",
|
||||||
|
"icon": "/img/home-2/visa/02.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Germany",
|
||||||
|
"slug": "germany",
|
||||||
|
"icon": "/img/home-2/visa/12.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Spain",
|
||||||
|
"slug": "spain",
|
||||||
|
"icon": "/img/home-2/visa/13.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "South Korea",
|
||||||
|
"slug": "south-korea",
|
||||||
|
"icon": "/img/home-2/visa/14.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"name": "Japan",
|
||||||
|
"slug": "japan",
|
||||||
|
"icon": "/img/home-2/visa/15.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"name": "Croatia",
|
||||||
|
"slug": "croatia",
|
||||||
|
"icon": "/img/home-2/visa/16.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "England",
|
||||||
|
"slug": "england",
|
||||||
|
"icon": "/img/home-2/visa/17.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"name": "Indonesia",
|
||||||
|
"slug": "indonesia",
|
||||||
|
"icon": "/img/home-2/visa/18.png",
|
||||||
|
"services": [
|
||||||
|
"Student Visa & Admission",
|
||||||
|
"Work Visa – H1B",
|
||||||
|
"Work permit for Canada",
|
||||||
|
"Student Visa for Canada"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
233
models/visa.js
233
models/visa.js
@@ -0,0 +1,233 @@
|
|||||||
|
// models/visa.js
|
||||||
|
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
|
||||||
|
// ==================== SCHEMAS ====================
|
||||||
|
|
||||||
|
// VisaItem Schema
|
||||||
|
const VisaItemSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
description: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// VisaTypeCategory Schema
|
||||||
|
const VisaTypeCategorySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
category: { type: String, default: "" },
|
||||||
|
items: [VisaItemSchema],
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// VisaProcessStep Schema
|
||||||
|
const VisaProcessStepSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
number: { type: String, default: "" },
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
description: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// VisaProcess Schema
|
||||||
|
const VisaProcessSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
steps: [VisaProcessStepSchema],
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// VisaCategory Schema
|
||||||
|
const VisaCategorySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
steps: {
|
||||||
|
type: [[String]],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// VisaService Schema
|
||||||
|
const VisaServiceSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
steps: [VisaProcessStepSchema],
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// RelatedCountry Schema
|
||||||
|
const RelatedCountrySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
id: { type: Number, default: 0 },
|
||||||
|
name: { type: String, default: "" },
|
||||||
|
icon: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Phone Schema
|
||||||
|
const PhoneSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
label: { type: String, default: "" },
|
||||||
|
value: { type: String, default: "" },
|
||||||
|
link: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Email Schema
|
||||||
|
const EmailSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
label: { type: String, default: "" },
|
||||||
|
value: { type: String, default: "" },
|
||||||
|
link: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Location Schema
|
||||||
|
const LocationSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
label: { type: String, default: "" },
|
||||||
|
address: { type: String, default: "" },
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ContactInfo Schema
|
||||||
|
const ContactInfoSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
img: { type: String, default: "" },
|
||||||
|
sectionTitle: { type: String, default: "" },
|
||||||
|
helpText: { type: String, default: "" },
|
||||||
|
|
||||||
|
phone: {
|
||||||
|
type: PhoneSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: EmailSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
type: LocationSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ActiveCountry Schema
|
||||||
|
const ActiveCountrySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
id: { type: Number, default: 0 },
|
||||||
|
name: { type: String, default: "" },
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
mainImage: { type: String, default: "" },
|
||||||
|
description: { type: String, default: "" },
|
||||||
|
additionalInfo: { type: String, default: "" },
|
||||||
|
tagline: { type: String, default: "" },
|
||||||
|
visaTypes: [VisaTypeCategorySchema],
|
||||||
|
visaProcess: {
|
||||||
|
type: VisaProcessSchema,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
gallery: {
|
||||||
|
type: [String],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
visaCategories: {
|
||||||
|
type: VisaCategorySchema,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
visaService: {
|
||||||
|
type: VisaServiceSchema,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// DetailedView Schema
|
||||||
|
const DetailedViewSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
activeCountry: {
|
||||||
|
type: ActiveCountrySchema,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
relatedCountries: {
|
||||||
|
type: [RelatedCountrySchema],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
contactInfo: {
|
||||||
|
type: ContactInfoSchema,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== MAIN VISA COUNTRY SCHEMA ====================
|
||||||
|
|
||||||
|
// Main VisaCountry Schema (Individual country object)
|
||||||
|
const VisaCountrySchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
id: { type: Number, required: true, index: true },
|
||||||
|
name: { type: String, required: true, index: true },
|
||||||
|
slug: { type: String, required: true, unique: true, index: true },
|
||||||
|
icon: { type: String, default: "" },
|
||||||
|
services: {
|
||||||
|
type: [String],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
detailedView: {
|
||||||
|
type: DetailedViewSchema,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== HERO SCHEMA ====================
|
||||||
|
|
||||||
|
const HeroSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
title: { type: String, default: "Visa" },
|
||||||
|
summaryList: {
|
||||||
|
type: [VisaCountrySchema],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== MAIN VISA SCHEMA ====================
|
||||||
|
|
||||||
|
const visaDataSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
hero: {
|
||||||
|
type: HeroSchema,
|
||||||
|
default: () => ({ title: "Visa", summaryList: [] }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== INDEXES ====================
|
||||||
|
|
||||||
|
visaDataSchema.index({ "hero.summaryList.slug": 1 });
|
||||||
|
visaDataSchema.index({ "hero.summaryList.id": 1 });
|
||||||
|
visaDataSchema.index({ "hero.summaryList.name": 1 });
|
||||||
|
|
||||||
|
// ==================== MODEL ====================
|
||||||
|
|
||||||
|
module.exports = mongoose.models.Visa || mongoose.model("Visa", visaDataSchema);
|
||||||
|
|||||||
174
routes/admin.js
174
routes/admin.js
@@ -16,7 +16,7 @@ const settingController = require("../controllers/settingController");
|
|||||||
const faqController = require("../controllers/faqController"); // Thêm import này
|
const faqController = require("../controllers/faqController"); // Thêm import này
|
||||||
const termsController = require("../controllers/termsController");
|
const termsController = require("../controllers/termsController");
|
||||||
const travelController = require("../controllers/travelController");
|
const travelController = require("../controllers/travelController");
|
||||||
|
const visaController = require("../controllers/visaController");
|
||||||
const { upload, uploadVideo, convertToWebp } = require("../middleware/upload");
|
const { upload, uploadVideo, convertToWebp } = require("../middleware/upload");
|
||||||
const safetyController = require("../controllers/safetyController");
|
const safetyController = require("../controllers/safetyController");
|
||||||
const insuranceController = require("../controllers/insuranceController");
|
const insuranceController = require("../controllers/insuranceController");
|
||||||
@@ -46,28 +46,28 @@ router.get("/about-us", ensureAuthenticated, aboutUsController.index);
|
|||||||
router.get(
|
router.get(
|
||||||
"/about-us/create",
|
"/about-us/create",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
aboutUsController.createForm
|
aboutUsController.createForm,
|
||||||
);
|
);
|
||||||
router.post("/about-us/create", ensureAuthenticated, aboutUsController.create);
|
router.post("/about-us/create", ensureAuthenticated, aboutUsController.create);
|
||||||
router.get(
|
router.get(
|
||||||
"/about-us/:id/edit",
|
"/about-us/:id/edit",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
aboutUsController.editForm
|
aboutUsController.editForm,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/about-us/:id/update",
|
"/about-us/:id/update",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
aboutUsController.update
|
aboutUsController.update,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/about-us/:id/delete",
|
"/about-us/:id/delete",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
aboutUsController.delete
|
aboutUsController.delete,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/about-us/:id/preview",
|
"/about-us/:id/preview",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
aboutUsController.preview
|
aboutUsController.preview,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Booking admin CRUD removed
|
// Booking admin CRUD removed
|
||||||
@@ -77,7 +77,7 @@ router.get("/form", ensureAuthenticated, formController.index);
|
|||||||
router.post(
|
router.post(
|
||||||
"/form/update",
|
"/form/update",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
formController.updateDefaultForm
|
formController.updateDefaultForm,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Upload routes
|
// Upload routes
|
||||||
@@ -93,23 +93,23 @@ router.post(
|
|||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
upload.single("image"),
|
upload.single("image"),
|
||||||
// convertToWebp, // Disabled to keep original image format (JPG/PNG)
|
// convertToWebp, // Disabled to keep original image format (JPG/PNG)
|
||||||
uploadController.uploadImage
|
uploadController.uploadImage,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/upload/video",
|
"/upload/video",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
uploadVideo.single("video"),
|
uploadVideo.single("video"),
|
||||||
uploadController.uploadVideo
|
uploadController.uploadVideo,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/upload/update-path",
|
"/upload/update-path",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
uploadController.updateImagePath
|
uploadController.updateImagePath,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/upload/delete",
|
"/upload/delete",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
uploadController.deleteImage
|
uploadController.deleteImage,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Header routes
|
// Header routes
|
||||||
@@ -118,22 +118,22 @@ router.post("/header/update", ensureAuthenticated, headerController.update);
|
|||||||
router.post(
|
router.post(
|
||||||
"/header/update-menu",
|
"/header/update-menu",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
headerController.updateMenu
|
headerController.updateMenu,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/header/menu-tree",
|
"/header/menu-tree",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
headerController.getMenuTree
|
headerController.getMenuTree,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/header/programmes/:menuId",
|
"/header/programmes/:menuId",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
headerController.getProgrammesByMenuId
|
headerController.getProgrammesByMenuId,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/header/menu-item/:menuId",
|
"/header/menu-item/:menuId",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
headerController.getMenuItem
|
headerController.getMenuItem,
|
||||||
);
|
);
|
||||||
router.get("/header/data", ensureAuthenticated, headerController.getHeaderData);
|
router.get("/header/data", ensureAuthenticated, headerController.getHeaderData);
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ router.post("/contact/update", ensureAuthenticated, contactController.update);
|
|||||||
router.get(
|
router.get(
|
||||||
"/contact/data",
|
"/contact/data",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
contactController.getContactData
|
contactController.getContactData,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Activity CRUD routes
|
// Activity CRUD routes
|
||||||
@@ -156,81 +156,81 @@ router.get("/activity", ensureAuthenticated, activityController.index);
|
|||||||
router.get(
|
router.get(
|
||||||
"/activity/create",
|
"/activity/create",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.createForm
|
activityController.createForm,
|
||||||
);
|
);
|
||||||
router.post("/activity/create", ensureAuthenticated, activityController.create);
|
router.post("/activity/create", ensureAuthenticated, activityController.create);
|
||||||
// Update filters (place before any parameterized /activity/:id routes to avoid route collision)
|
// Update filters (place before any parameterized /activity/:id routes to avoid route collision)
|
||||||
router.post(
|
router.post(
|
||||||
"/activity/filters/update",
|
"/activity/filters/update",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.updateFilters
|
activityController.updateFilters,
|
||||||
);
|
);
|
||||||
// Update hero (global hero section for activities)
|
// Update hero (global hero section for activities)
|
||||||
router.post(
|
router.post(
|
||||||
"/activity/hero/update",
|
"/activity/hero/update",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.updateHero
|
activityController.updateHero,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/activity/:id/edit",
|
"/activity/:id/edit",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.editForm
|
activityController.editForm,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/activity/:id/update",
|
"/activity/:id/update",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.update
|
activityController.update,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/activity/:id/delete",
|
"/activity/:id/delete",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.delete
|
activityController.delete,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/activity/:id/toggle-status",
|
"/activity/:id/toggle-status",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.toggleStatus
|
activityController.toggleStatus,
|
||||||
);
|
);
|
||||||
// Update display order
|
// Update display order
|
||||||
router.post(
|
router.post(
|
||||||
"/activity/update-order",
|
"/activity/update-order",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.updateOrder
|
activityController.updateOrder,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Booking submissions routes
|
// Booking submissions routes
|
||||||
router.get(
|
router.get(
|
||||||
"/activity/:id/bookings/count",
|
"/activity/:id/bookings/count",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.getBookingCount
|
activityController.getBookingCount,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/activity/:id/bookings",
|
"/activity/:id/bookings",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.getBookingSubmissions
|
activityController.getBookingSubmissions,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/activity/:id/bookings/export",
|
"/activity/:id/bookings/export",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.exportBookingData
|
activityController.exportBookingData,
|
||||||
);
|
);
|
||||||
// Export all bookings (across all activities)
|
// Export all bookings (across all activities)
|
||||||
router.get(
|
router.get(
|
||||||
"/bookings/export-all",
|
"/bookings/export-all",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.exportAllBookingsData
|
activityController.exportAllBookingsData,
|
||||||
);
|
);
|
||||||
// Update booking submission
|
// Update booking submission
|
||||||
router.put(
|
router.put(
|
||||||
"/bookings/:bookingId",
|
"/bookings/:bookingId",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
bookingSubmissionController.updateBookingSubmission
|
bookingSubmissionController.updateBookingSubmission,
|
||||||
);
|
);
|
||||||
// Delete booking submission
|
// Delete booking submission
|
||||||
router.delete(
|
router.delete(
|
||||||
"/bookings/:bookingId",
|
"/bookings/:bookingId",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
bookingSubmissionController.deleteBookingSubmission
|
bookingSubmissionController.deleteBookingSubmission,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update filters
|
// Update filters
|
||||||
@@ -239,7 +239,7 @@ router.delete(
|
|||||||
router.get(
|
router.get(
|
||||||
"/activity/:id/preview",
|
"/activity/:id/preview",
|
||||||
ensureAuthenticated,
|
ensureAuthenticated,
|
||||||
activityController.preview
|
activityController.preview,
|
||||||
);
|
);
|
||||||
|
|
||||||
// FAQ routes - Thêm vào đây
|
// FAQ routes - Thêm vào đây
|
||||||
@@ -250,8 +250,16 @@ router.get("/faq/api", faqController.api);
|
|||||||
|
|
||||||
// API routes cho quản lý FAQ items (AJAX calls)
|
// API routes cho quản lý FAQ items (AJAX calls)
|
||||||
router.post("/faq/api/add-faq", ensureAuthenticated, faqController.addFAQ);
|
router.post("/faq/api/add-faq", ensureAuthenticated, faqController.addFAQ);
|
||||||
router.put("/faq/api/update-faq-item/:sectionId/:faqId", ensureAuthenticated, faqController.updateFAQItem);
|
router.put(
|
||||||
router.delete("/faq/api/delete-faq-item/:sectionId/:faqId", ensureAuthenticated, faqController.deleteFAQItem);
|
"/faq/api/update-faq-item/:sectionId/:faqId",
|
||||||
|
ensureAuthenticated,
|
||||||
|
faqController.updateFAQItem,
|
||||||
|
);
|
||||||
|
router.delete(
|
||||||
|
"/faq/api/delete-faq-item/:sectionId/:faqId",
|
||||||
|
ensureAuthenticated,
|
||||||
|
faqController.deleteFAQItem,
|
||||||
|
);
|
||||||
router.get("/terms-conditions", ensureAuthenticated, termsController.index);
|
router.get("/terms-conditions", ensureAuthenticated, termsController.index);
|
||||||
router.post("/terms/update", ensureAuthenticated, termsController.update);
|
router.post("/terms/update", ensureAuthenticated, termsController.update);
|
||||||
router.get("/terms/data", ensureAuthenticated, termsController.getTermsData);
|
router.get("/terms/data", ensureAuthenticated, termsController.getTermsData);
|
||||||
@@ -267,42 +275,70 @@ router.get("/travel/api", travelController.api);
|
|||||||
router.get("/travel/seed", ensureAuthenticated, travelController.seed);
|
router.get("/travel/seed", ensureAuthenticated, travelController.seed);
|
||||||
|
|
||||||
// API routes cho quản lý FAQ sections (AJAX calls)
|
// API routes cho quản lý FAQ sections (AJAX calls)
|
||||||
router.post("/faq/api/add-section", ensureAuthenticated, faqController.addFAQSection);
|
router.post(
|
||||||
router.put("/faq/api/update-section/:sectionId", ensureAuthenticated, faqController.updateFAQSection);
|
"/faq/api/add-section",
|
||||||
router.delete("/faq/api/delete-section/:sectionId", ensureAuthenticated, faqController.deleteFAQSection);
|
ensureAuthenticated,
|
||||||
router.post("/faq/api/reorder-sections", ensureAuthenticated, faqController.reorderFAQSection);
|
faqController.addFAQSection,
|
||||||
|
);
|
||||||
|
router.put(
|
||||||
|
"/faq/api/update-section/:sectionId",
|
||||||
|
ensureAuthenticated,
|
||||||
|
faqController.updateFAQSection,
|
||||||
|
);
|
||||||
|
router.delete(
|
||||||
|
"/faq/api/delete-section/:sectionId",
|
||||||
|
ensureAuthenticated,
|
||||||
|
faqController.deleteFAQSection,
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
"/faq/api/reorder-sections",
|
||||||
|
ensureAuthenticated,
|
||||||
|
faqController.reorderFAQSection,
|
||||||
|
);
|
||||||
|
|
||||||
// API routes cho sidebar navigation (AJAX calls)
|
// API routes cho sidebar navigation (AJAX calls)
|
||||||
router.put("/faq/api/update-sidebar", ensureAuthenticated, faqController.updateSidebarNav);
|
router.put(
|
||||||
|
"/faq/api/update-sidebar",
|
||||||
|
ensureAuthenticated,
|
||||||
|
faqController.updateSidebarNav,
|
||||||
|
);
|
||||||
|
|
||||||
// Safety routes
|
// Safety routes
|
||||||
router.get("/safety", ensureAuthenticated, safetyController.index);
|
router.get("/safety", ensureAuthenticated, safetyController.index);
|
||||||
router.post("/safety/update", ensureAuthenticated, safetyController.update);
|
router.post("/safety/update", ensureAuthenticated, safetyController.update);
|
||||||
// Camp Location routes
|
// Camp Location routes
|
||||||
router.get("/camp-location", ensureAuthenticated, campLocationController.index);
|
router.get("/camp-location", ensureAuthenticated, campLocationController.index);
|
||||||
router.post("/camp-location/update", ensureAuthenticated, campLocationController.update);
|
router.post(
|
||||||
|
"/camp-location/update",
|
||||||
|
ensureAuthenticated,
|
||||||
|
campLocationController.update,
|
||||||
|
);
|
||||||
|
|
||||||
//Insurance routes
|
//Insurance routes
|
||||||
router.get("/insurance", ensureAuthenticated, insuranceController.index);
|
router.get("/insurance", ensureAuthenticated, insuranceController.index);
|
||||||
router.post("/insurance/update", ensureAuthenticated, insuranceController.update);
|
router.post(
|
||||||
|
"/insurance/update",
|
||||||
|
ensureAuthenticated,
|
||||||
|
insuranceController.update,
|
||||||
|
);
|
||||||
// Test Image Paths route
|
// Test Image Paths route
|
||||||
router.get("/test-images", ensureAuthenticated, (req, res) => {
|
router.get("/test-images", ensureAuthenticated, (req, res) => {
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const campLocationData = require('../data/camp-location.json');
|
const campLocationData = require("../data/camp-location.json");
|
||||||
|
|
||||||
// Collect all image paths
|
// Collect all image paths
|
||||||
const imagePaths = [];
|
const imagePaths = [];
|
||||||
|
|
||||||
// Camps images
|
// Camps images
|
||||||
if (campLocationData.camps) {
|
if (campLocationData.camps) {
|
||||||
campLocationData.camps.forEach(camp => {
|
campLocationData.camps.forEach((camp) => {
|
||||||
if (camp.image) {
|
if (camp.image) {
|
||||||
imagePaths.push({
|
imagePaths.push({
|
||||||
type: 'Camp',
|
type: "Camp",
|
||||||
name: camp.title,
|
name: camp.title,
|
||||||
path: camp.image,
|
path: camp.image,
|
||||||
exists: fs.existsSync(path.join(__dirname, '../public', camp.image))
|
exists: fs.existsSync(path.join(__dirname, "../public", camp.image)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -310,25 +346,29 @@ router.get("/test-images", ensureAuthenticated, (req, res) => {
|
|||||||
|
|
||||||
// Locations images
|
// Locations images
|
||||||
if (campLocationData.locations) {
|
if (campLocationData.locations) {
|
||||||
campLocationData.locations.forEach(location => {
|
campLocationData.locations.forEach((location) => {
|
||||||
if (location.imageSrc) {
|
if (location.imageSrc) {
|
||||||
imagePaths.push({
|
imagePaths.push({
|
||||||
type: 'Location',
|
type: "Location",
|
||||||
name: location.title,
|
name: location.title,
|
||||||
path: location.imageSrc,
|
path: location.imageSrc,
|
||||||
exists: fs.existsSync(path.join(__dirname, '../public', location.imageSrc))
|
exists: fs.existsSync(
|
||||||
|
path.join(__dirname, "../public", location.imageSrc),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Program images
|
// Program images
|
||||||
if (location.programOptions) {
|
if (location.programOptions) {
|
||||||
location.programOptions.forEach(program => {
|
location.programOptions.forEach((program) => {
|
||||||
if (program.imageSrc) {
|
if (program.imageSrc) {
|
||||||
imagePaths.push({
|
imagePaths.push({
|
||||||
type: 'Program',
|
type: "Program",
|
||||||
name: program.title,
|
name: program.title,
|
||||||
path: program.imageSrc,
|
path: program.imageSrc,
|
||||||
exists: fs.existsSync(path.join(__dirname, '../public', program.imageSrc))
|
exists: fs.existsSync(
|
||||||
|
path.join(__dirname, "../public", program.imageSrc),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -336,12 +376,34 @@ router.get("/test-images", ensureAuthenticated, (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('admin/test-images', {
|
res.render("admin/test-images", {
|
||||||
layout: 'layouts/admin',
|
layout: "layouts/admin",
|
||||||
title: 'Test Image Paths',
|
title: "Test Image Paths",
|
||||||
images: imagePaths,
|
images: imagePaths,
|
||||||
user: req.session.user
|
user: req.session.user,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Display visa management page
|
||||||
|
router.get("/visa", visaController.index);
|
||||||
|
|
||||||
|
// Update hero title
|
||||||
|
router.post("/visa/update", ensureAuthenticated, visaController.update);
|
||||||
|
|
||||||
|
// Add new country
|
||||||
|
router.post("/visa/add", ensureAuthenticated, visaController.addCountry);
|
||||||
|
|
||||||
|
// Update single country
|
||||||
|
router.put(
|
||||||
|
"/visa/update/:slug",
|
||||||
|
ensureAuthenticated,
|
||||||
|
visaController.updateCountry,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete country
|
||||||
|
router.delete(
|
||||||
|
"/delete/:slug",
|
||||||
|
ensureAuthenticated,
|
||||||
|
visaController.deleteCountry,
|
||||||
|
);
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ const headerController = require("../controllers/headerController");
|
|||||||
const footerController = require("../controllers/footerController");
|
const footerController = require("../controllers/footerController");
|
||||||
const contactController = require("../controllers/contactController");
|
const contactController = require("../controllers/contactController");
|
||||||
const faqController = require("../controllers/faqController");
|
const faqController = require("../controllers/faqController");
|
||||||
|
const visaController = require("../controllers/visaController");
|
||||||
const safetyController = require("../controllers/safetyController");
|
const safetyController = require("../controllers/safetyController");
|
||||||
const campLocationController = require("../controllers/campLocationController");
|
const campLocationController = require("../controllers/campLocationController");
|
||||||
// Booking flow removed
|
// Booking flow removed
|
||||||
|
|
||||||
const insuranceController= require("../controllers/insuranceController");
|
const insuranceController = require("../controllers/insuranceController");
|
||||||
const termsController = require("../controllers/termsController"); // <-- IMPORT ĐÃ CÓ
|
const termsController = require("../controllers/termsController"); // <-- IMPORT ĐÃ CÓ
|
||||||
const activityController = require("../controllers/activityController");
|
const activityController = require("../controllers/activityController");
|
||||||
const travelController = require("../controllers/travelController");
|
const travelController = require("../controllers/travelController");
|
||||||
@@ -61,8 +61,7 @@ router.get("/api/activities/:id", activityController.apiDetail);
|
|||||||
router.get("/api/camp-location", campLocationController.api);
|
router.get("/api/camp-location", campLocationController.api);
|
||||||
// Booking routes removed
|
// Booking routes removed
|
||||||
// Insurance APi route
|
// Insurance APi route
|
||||||
router.get("/api/insurance", insuranceController.api)
|
router.get("/api/insurance", insuranceController.api);
|
||||||
|
|
||||||
|
|
||||||
router.get("/api/terms", termsController.api);
|
router.get("/api/terms", termsController.api);
|
||||||
|
|
||||||
@@ -95,33 +94,55 @@ router.get("/api/travel", travelController.api);
|
|||||||
|
|
||||||
// Booking submission APIs (public endpoints)
|
// Booking submission APIs (public endpoints)
|
||||||
router.post("/api/booking/submit", bookingSubmissionController.submitBooking);
|
router.post("/api/booking/submit", bookingSubmissionController.submitBooking);
|
||||||
router.get("/api/activity/:activityId/sessions", bookingSubmissionController.getAvailableSessions);
|
router.get(
|
||||||
router.get("/api/activity/:activityId/session/:sessionId/availability", bookingSubmissionController.getSessionAvailability);
|
"/api/activity/:activityId/sessions",
|
||||||
|
bookingSubmissionController.getAvailableSessions,
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
"/api/activity/:activityId/session/:sessionId/availability",
|
||||||
|
bookingSubmissionController.getSessionAvailability,
|
||||||
|
);
|
||||||
|
|
||||||
// New API for creating bookings directly into camp sessions (by program)
|
// New API for creating bookings directly into camp sessions (by program)
|
||||||
router.post(
|
router.post(
|
||||||
"/api/camps/:program/sessions/:sessionId/bookings",
|
"/api/camps/:program/sessions/:sessionId/bookings",
|
||||||
activityController.createSessionBookingByProgram
|
activityController.createSessionBookingByProgram,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/api/camps/:program/sessions/:sessionId/bookings",
|
"/api/camps/:program/sessions/:sessionId/bookings",
|
||||||
activityController.getSessionBookingsByProgram
|
activityController.getSessionBookingsByProgram,
|
||||||
);
|
);
|
||||||
// Keep admin-style update/delete by activityId (protected) if needed
|
// Keep admin-style update/delete by activityId (protected) if needed
|
||||||
router.put("/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", activityController.updateSessionBooking);
|
router.put(
|
||||||
router.delete("/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", activityController.deleteSessionBooking);
|
"/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId",
|
||||||
|
activityController.updateSessionBooking,
|
||||||
|
);
|
||||||
|
router.delete(
|
||||||
|
"/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId",
|
||||||
|
activityController.deleteSessionBooking,
|
||||||
|
);
|
||||||
|
|
||||||
// Demo booking form
|
// Demo booking form
|
||||||
router.get("/demo/booking-form", (req, res) => {
|
router.get("/demo/booking-form", (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, '../views/demo/booking-form.html'));
|
res.sendFile(path.join(__dirname, "../views/demo/booking-form.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Demo session booking API
|
// Demo session booking API
|
||||||
router.get("/demo/session-booking-api", (req, res) => {
|
router.get("/demo/session-booking-api", (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, '../views/demo/session-booking-api.html'));
|
res.sendFile(path.join(__dirname, "../views/demo/session-booking-api.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
// // API route cho blog detail
|
// // API route cho blog detail
|
||||||
// router.get('/api/blog-detail', blogDetailController.api);
|
// router.get('/api/blog-detail', blogDetailController.api);
|
||||||
|
|
||||||
|
// ==================== PUBLIC API ROUTES ====================
|
||||||
|
|
||||||
|
// 1. Đưa các route cụ thể (chi tiết nhất) lên đầu tiên
|
||||||
|
// 2. Route lấy TOÀN BỘ dữ liệu (phải nằm trên route :slug)
|
||||||
|
router.get("/api/visa", visaController.api);
|
||||||
|
router.get("/api/visa/hero", visaController.apiHero);
|
||||||
|
router.get("/api/visa/countries", visaController.apiCountries);
|
||||||
|
|
||||||
|
// 3. Route lấy chi tiết theo slug (luôn để dưới cùng của nhóm này)
|
||||||
|
router.get("/api/visa/:slug", visaController.apiCountry);
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
336
scripts/2026_02_03_645124_visa.js
Normal file
336
scripts/2026_02_03_645124_visa.js
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
// scripts/migrateVisa.js
|
||||||
|
|
||||||
|
require("dotenv").config();
|
||||||
|
const fs = require("fs").promises;
|
||||||
|
const path = require("path");
|
||||||
|
const mongoose = require("mongoose");
|
||||||
|
const Visa = require("../models/visa");
|
||||||
|
|
||||||
|
// 1. Đọc file JSON
|
||||||
|
async function loadVisaData() {
|
||||||
|
const filePath = path.join(__dirname, "..", "data", "visa.json");
|
||||||
|
const raw = await fs.readFile(filePath, "utf8");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Hàm Transform: Đổ dữ liệu từ JSON vào đúng Schema
|
||||||
|
function transformVisa(sourceData) {
|
||||||
|
// JSON có structure hero.title và hero.summaryList
|
||||||
|
return {
|
||||||
|
hero: {
|
||||||
|
title: sourceData.hero?.title || "Visa",
|
||||||
|
summaryList: Array.isArray(sourceData.hero?.summaryList)
|
||||||
|
? sourceData.hero.summaryList.map((country) =>
|
||||||
|
transformCountry(country),
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform individual country
|
||||||
|
function transformCountry(source) {
|
||||||
|
return {
|
||||||
|
id: source.id || 0,
|
||||||
|
name: source.name || "",
|
||||||
|
slug: source.slug || "",
|
||||||
|
icon: source.icon || "",
|
||||||
|
services: Array.isArray(source.services) ? source.services : [],
|
||||||
|
detailedView: source.detailedView
|
||||||
|
? transformDetailedView(source.detailedView)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform DetailedView
|
||||||
|
function transformDetailedView(source) {
|
||||||
|
return {
|
||||||
|
activeCountry: source.activeCountry
|
||||||
|
? transformActiveCountry(source.activeCountry)
|
||||||
|
: null,
|
||||||
|
relatedCountries: Array.isArray(source.relatedCountries)
|
||||||
|
? source.relatedCountries.map((country) => ({
|
||||||
|
id: country.id || 0,
|
||||||
|
name: country.name || "",
|
||||||
|
icon: country.icon || "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
contactInfo: source.contactInfo
|
||||||
|
? transformContactInfo(source.contactInfo)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform ActiveCountry
|
||||||
|
function transformActiveCountry(source) {
|
||||||
|
return {
|
||||||
|
id: source.id || 0,
|
||||||
|
name: source.name || "",
|
||||||
|
title: source.title || "",
|
||||||
|
mainImage: source.mainImage || "",
|
||||||
|
description: source.description || "",
|
||||||
|
additionalInfo: source.additionalInfo || "",
|
||||||
|
tagline: source.tagline || "",
|
||||||
|
visaTypes: Array.isArray(source.visaTypes)
|
||||||
|
? source.visaTypes.map((type) => ({
|
||||||
|
category: type.category || "",
|
||||||
|
items: Array.isArray(type.items)
|
||||||
|
? type.items.map((item) => ({
|
||||||
|
title: item.title || "",
|
||||||
|
description: item.description || "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
visaProcess: source.visaProcess
|
||||||
|
? transformVisaProcess(source.visaProcess)
|
||||||
|
: null,
|
||||||
|
gallery: Array.isArray(source.gallery) ? source.gallery : [],
|
||||||
|
visaCategories: source.visaCategories
|
||||||
|
? transformVisaCategories(source.visaCategories)
|
||||||
|
: null,
|
||||||
|
visaService: source.visaService
|
||||||
|
? transformVisaService(source.visaService)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform VisaProcess
|
||||||
|
function transformVisaProcess(source) {
|
||||||
|
return {
|
||||||
|
title: source.title || "",
|
||||||
|
steps: Array.isArray(source.steps)
|
||||||
|
? source.steps.map((step) => ({
|
||||||
|
number: step.number || "",
|
||||||
|
title: step.title || "",
|
||||||
|
description: step.description || "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform VisaCategories
|
||||||
|
function transformVisaCategories(source) {
|
||||||
|
return {
|
||||||
|
title: source.title || "",
|
||||||
|
steps: Array.isArray(source.steps) ? source.steps : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform VisaService
|
||||||
|
function transformVisaService(source) {
|
||||||
|
return {
|
||||||
|
title: source.title || "",
|
||||||
|
steps: Array.isArray(source.steps)
|
||||||
|
? source.steps.map((step) => ({
|
||||||
|
number: step.number || "",
|
||||||
|
title: step.title || "",
|
||||||
|
description: step.description || "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Transform ContactInfo
|
||||||
|
function transformContactInfo(source) {
|
||||||
|
return {
|
||||||
|
img: source.img || "",
|
||||||
|
sectionTitle: source.sectionTitle || "Visa & Immigration",
|
||||||
|
helpText: source.helpText || "Need Help?",
|
||||||
|
phone: {
|
||||||
|
label: source.phone?.label || "Call Us",
|
||||||
|
value: source.phone?.value || "",
|
||||||
|
link: source.phone?.link || "",
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
label: source.email?.label || "Mail Us",
|
||||||
|
value: source.email?.value || "",
|
||||||
|
link: source.email?.link || "",
|
||||||
|
},
|
||||||
|
location: {
|
||||||
|
label: source.location?.label || "Location",
|
||||||
|
address: source.location?.address || "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate data before migration
|
||||||
|
function validateVisaData(visaData) {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!visaData.hero) {
|
||||||
|
errors.push("Missing hero section");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!visaData.hero?.title) {
|
||||||
|
console.warn("⚠️ Hero title is missing, using default 'Visa'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(visaData.hero?.summaryList)) {
|
||||||
|
errors.push("summaryList must be an array");
|
||||||
|
} else if (visaData.hero.summaryList.length === 0) {
|
||||||
|
errors.push("summaryList is empty");
|
||||||
|
} else {
|
||||||
|
// Validate each country
|
||||||
|
visaData.hero.summaryList.forEach((country, idx) => {
|
||||||
|
if (!country.name || !country.slug) {
|
||||||
|
errors.push(`Country at index ${idx}: missing name or slug`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (country.detailedView) {
|
||||||
|
if (!country.detailedView.activeCountry) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Country "${country.name}" (${idx}): missing activeCountry details`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(country.detailedView.relatedCountries)) {
|
||||||
|
errors.push(
|
||||||
|
`Country "${country.name}" (${idx}): relatedCountries must be array`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function: Get data summary
|
||||||
|
function getDataSummary(visaData) {
|
||||||
|
const summary = {
|
||||||
|
heroTitle: visaData.hero?.title || "N/A",
|
||||||
|
totalCountries: visaData.hero?.summaryList?.length || 0,
|
||||||
|
withDetails: 0,
|
||||||
|
withoutDetails: 0,
|
||||||
|
byCountry: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visaData.hero?.summaryList) {
|
||||||
|
visaData.hero.summaryList.forEach((country) => {
|
||||||
|
const hasDetails = !!country.detailedView?.activeCountry;
|
||||||
|
const relatedCount = country.detailedView?.relatedCountries?.length || 0;
|
||||||
|
|
||||||
|
if (hasDetails) {
|
||||||
|
summary.withDetails++;
|
||||||
|
} else {
|
||||||
|
summary.withoutDetails++;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.byCountry.push({
|
||||||
|
name: country.name,
|
||||||
|
slug: country.slug,
|
||||||
|
hasDetails,
|
||||||
|
relatedCountries: relatedCount,
|
||||||
|
services: country.services?.length || 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Chạy Migration
|
||||||
|
async function migrate() {
|
||||||
|
try {
|
||||||
|
// Kết nối DB
|
||||||
|
console.log("🔗 Connecting to MongoDB...");
|
||||||
|
await mongoose.connect(process.env.MONGODB_URI);
|
||||||
|
console.log("✅ Connected to MongoDB\n");
|
||||||
|
|
||||||
|
// A. Lấy dữ liệu thô
|
||||||
|
console.log("📖 Loading visa data from JSON...");
|
||||||
|
const rawData = await loadVisaData();
|
||||||
|
console.log("✅ JSON data loaded\n");
|
||||||
|
|
||||||
|
// B. Chuẩn hóa dữ liệu theo Schema
|
||||||
|
console.log("🔄 Transforming data structure...");
|
||||||
|
const visaData = transformVisa(rawData);
|
||||||
|
console.log("✅ Data transformation completed\n");
|
||||||
|
|
||||||
|
// C. Validate dữ liệu
|
||||||
|
console.log("✔️ Validating data structure...");
|
||||||
|
const errors = validateVisaData(visaData);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error("❌ Validation errors found:");
|
||||||
|
errors.forEach((err, idx) => console.error(` ${idx + 1}. ${err}`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log("✅ Data validation passed\n");
|
||||||
|
|
||||||
|
// D. Get summary
|
||||||
|
const summary = getDataSummary(visaData);
|
||||||
|
|
||||||
|
console.log("📊 Migration Summary:");
|
||||||
|
console.log(` Hero Title: "${summary.heroTitle}"`);
|
||||||
|
console.log(` Total countries: ${summary.totalCountries}`);
|
||||||
|
console.log(` With details: ${summary.withDetails}`);
|
||||||
|
console.log(` Without details: ${summary.withoutDetails}`);
|
||||||
|
console.log(`\n Country Details:`);
|
||||||
|
|
||||||
|
summary.byCountry.forEach((country) => {
|
||||||
|
const detailBadge = country.hasDetails ? "✅" : "❌";
|
||||||
|
const detailText = country.hasDetails
|
||||||
|
? `(${country.relatedCountries} related)`
|
||||||
|
: "(basic only)";
|
||||||
|
console.log(
|
||||||
|
` ${detailBadge} ${country.name.padEnd(20)} (${country.slug.padEnd(
|
||||||
|
12,
|
||||||
|
)}) - ${country.services} services ${detailText}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
// E. Lưu vào DB (Upsert: Có rồi thì update, chưa có thì tạo)
|
||||||
|
const existingDoc = await Visa.findOne().sort({ updatedAt: -1 });
|
||||||
|
|
||||||
|
if (existingDoc) {
|
||||||
|
console.log("📝 Updating existing Visa document...");
|
||||||
|
console.log(` Document ID: ${existingDoc._id}`);
|
||||||
|
|
||||||
|
const updated = await Visa.findByIdAndUpdate(
|
||||||
|
existingDoc._id,
|
||||||
|
{ $set: visaData },
|
||||||
|
{ new: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ Visa document updated successfully");
|
||||||
|
console.log(` Updated at: ${updated.updatedAt}`);
|
||||||
|
} else {
|
||||||
|
console.log("📝 Creating NEW Visa document...");
|
||||||
|
|
||||||
|
const newDoc = await Visa.create(visaData);
|
||||||
|
|
||||||
|
console.log("✅ Visa document created successfully");
|
||||||
|
console.log(` Document ID: ${newDoc._id}`);
|
||||||
|
console.log(` Created at: ${newDoc.createdAt}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✨ Visa migration completed successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\n❌ Migration failed:");
|
||||||
|
console.error(` Error: ${error.message}`);
|
||||||
|
|
||||||
|
if (error.name === "ValidationError") {
|
||||||
|
console.error("\n Validation Errors:");
|
||||||
|
Object.keys(error.errors).forEach((field) => {
|
||||||
|
console.error(` - ${field}: ${error.errors[field].message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.stack) {
|
||||||
|
console.error("\n📋 Stack trace:");
|
||||||
|
console.error(error.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await mongoose.connection.close();
|
||||||
|
console.log("\n🔌 MongoDB connection closed");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
console.log("🚀 Starting Visa Migration...\n");
|
||||||
|
migrate();
|
||||||
11
server.js
11
server.js
@@ -32,16 +32,7 @@ app.set("layout extractStyles", true);
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
"/assets",
|
|
||||||
(req, res, next) => {
|
|
||||||
// Cho phép mọi domain truy cập tài nguyên tĩnh
|
|
||||||
res.header("Access-Control-Allow-Origin", "*");
|
|
||||||
res.header("Access-Control-Allow-Methods", "GET");
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
express.static(path.join(__dirname, "assets")),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Session configuration
|
// Session configuration
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
1231
views/admin/visa/index.ejs
Normal file
1231
views/admin/visa/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -796,6 +796,13 @@
|
|||||||
>Activity & Booking</a
|
>Activity & Booking</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link <%= currentPath === '/admin/visa' ? 'active' : '' %>"
|
||||||
|
href="/admin/visa"
|
||||||
|
>Visa</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
Reference in New Issue
Block a user