diff --git a/controllers/visaController.js b/controllers/visaController.js
new file mode 100644
index 0000000..77ef292
--- /dev/null
+++ b/controllers/visaController.js
@@ -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",
+ });
+ }
+};
diff --git a/data/visa.json b/data/visa.json
new file mode 100644
index 0000000..f39e1d5
--- /dev/null
+++ b/data/visa.json
@@ -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"
+ ]
+ }
+ ]
+ }
+}
diff --git a/models/visa.js b/models/visa.js
index e69de29..6edae4e 100644
--- a/models/visa.js
+++ b/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);
diff --git a/routes/admin.js b/routes/admin.js
index d41e56d..a22528e 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -16,7 +16,7 @@ const settingController = require("../controllers/settingController");
const faqController = require("../controllers/faqController"); // Thêm import này
const termsController = require("../controllers/termsController");
const travelController = require("../controllers/travelController");
-
+const visaController = require("../controllers/visaController");
const { upload, uploadVideo, convertToWebp } = require("../middleware/upload");
const safetyController = require("../controllers/safetyController");
const insuranceController = require("../controllers/insuranceController");
@@ -46,28 +46,28 @@ router.get("/about-us", ensureAuthenticated, aboutUsController.index);
router.get(
"/about-us/create",
ensureAuthenticated,
- aboutUsController.createForm
+ aboutUsController.createForm,
);
router.post("/about-us/create", ensureAuthenticated, aboutUsController.create);
router.get(
"/about-us/:id/edit",
ensureAuthenticated,
- aboutUsController.editForm
+ aboutUsController.editForm,
);
router.post(
"/about-us/:id/update",
ensureAuthenticated,
- aboutUsController.update
+ aboutUsController.update,
);
router.post(
"/about-us/:id/delete",
ensureAuthenticated,
- aboutUsController.delete
+ aboutUsController.delete,
);
router.get(
"/about-us/:id/preview",
ensureAuthenticated,
- aboutUsController.preview
+ aboutUsController.preview,
);
// Booking admin CRUD removed
@@ -77,7 +77,7 @@ router.get("/form", ensureAuthenticated, formController.index);
router.post(
"/form/update",
ensureAuthenticated,
- formController.updateDefaultForm
+ formController.updateDefaultForm,
);
// Upload routes
@@ -93,23 +93,23 @@ router.post(
ensureAuthenticated,
upload.single("image"),
// convertToWebp, // Disabled to keep original image format (JPG/PNG)
- uploadController.uploadImage
+ uploadController.uploadImage,
);
router.post(
"/upload/video",
ensureAuthenticated,
uploadVideo.single("video"),
- uploadController.uploadVideo
+ uploadController.uploadVideo,
);
router.post(
"/upload/update-path",
ensureAuthenticated,
- uploadController.updateImagePath
+ uploadController.updateImagePath,
);
router.post(
"/upload/delete",
ensureAuthenticated,
- uploadController.deleteImage
+ uploadController.deleteImage,
);
// Header routes
@@ -118,22 +118,22 @@ router.post("/header/update", ensureAuthenticated, headerController.update);
router.post(
"/header/update-menu",
ensureAuthenticated,
- headerController.updateMenu
+ headerController.updateMenu,
);
router.get(
"/header/menu-tree",
ensureAuthenticated,
- headerController.getMenuTree
+ headerController.getMenuTree,
);
router.get(
"/header/programmes/:menuId",
ensureAuthenticated,
- headerController.getProgrammesByMenuId
+ headerController.getProgrammesByMenuId,
);
router.get(
"/header/menu-item/:menuId",
ensureAuthenticated,
- headerController.getMenuItem
+ headerController.getMenuItem,
);
router.get("/header/data", ensureAuthenticated, headerController.getHeaderData);
@@ -148,7 +148,7 @@ router.post("/contact/update", ensureAuthenticated, contactController.update);
router.get(
"/contact/data",
ensureAuthenticated,
- contactController.getContactData
+ contactController.getContactData,
);
// Activity CRUD routes
@@ -156,81 +156,81 @@ router.get("/activity", ensureAuthenticated, activityController.index);
router.get(
"/activity/create",
ensureAuthenticated,
- activityController.createForm
+ activityController.createForm,
);
router.post("/activity/create", ensureAuthenticated, activityController.create);
// Update filters (place before any parameterized /activity/:id routes to avoid route collision)
router.post(
"/activity/filters/update",
ensureAuthenticated,
- activityController.updateFilters
+ activityController.updateFilters,
);
// Update hero (global hero section for activities)
router.post(
"/activity/hero/update",
ensureAuthenticated,
- activityController.updateHero
+ activityController.updateHero,
);
router.get(
"/activity/:id/edit",
ensureAuthenticated,
- activityController.editForm
+ activityController.editForm,
);
router.post(
"/activity/:id/update",
ensureAuthenticated,
- activityController.update
+ activityController.update,
);
router.post(
"/activity/:id/delete",
ensureAuthenticated,
- activityController.delete
+ activityController.delete,
);
router.post(
"/activity/:id/toggle-status",
ensureAuthenticated,
- activityController.toggleStatus
+ activityController.toggleStatus,
);
// Update display order
router.post(
"/activity/update-order",
ensureAuthenticated,
- activityController.updateOrder
+ activityController.updateOrder,
);
// Booking submissions routes
router.get(
"/activity/:id/bookings/count",
ensureAuthenticated,
- activityController.getBookingCount
+ activityController.getBookingCount,
);
router.get(
"/activity/:id/bookings",
ensureAuthenticated,
- activityController.getBookingSubmissions
+ activityController.getBookingSubmissions,
);
router.get(
"/activity/:id/bookings/export",
ensureAuthenticated,
- activityController.exportBookingData
+ activityController.exportBookingData,
);
// Export all bookings (across all activities)
router.get(
"/bookings/export-all",
ensureAuthenticated,
- activityController.exportAllBookingsData
+ activityController.exportAllBookingsData,
);
// Update booking submission
router.put(
"/bookings/:bookingId",
ensureAuthenticated,
- bookingSubmissionController.updateBookingSubmission
+ bookingSubmissionController.updateBookingSubmission,
);
// Delete booking submission
router.delete(
"/bookings/:bookingId",
ensureAuthenticated,
- bookingSubmissionController.deleteBookingSubmission
+ bookingSubmissionController.deleteBookingSubmission,
);
// Update filters
@@ -239,7 +239,7 @@ router.delete(
router.get(
"/activity/:id/preview",
ensureAuthenticated,
- activityController.preview
+ activityController.preview,
);
// 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)
router.post("/faq/api/add-faq", ensureAuthenticated, faqController.addFAQ);
-router.put("/faq/api/update-faq-item/:sectionId/:faqId", ensureAuthenticated, faqController.updateFAQItem);
-router.delete("/faq/api/delete-faq-item/:sectionId/:faqId", ensureAuthenticated, faqController.deleteFAQItem);
+router.put(
+ "/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.post("/terms/update", ensureAuthenticated, termsController.update);
router.get("/terms/data", ensureAuthenticated, termsController.getTermsData);
@@ -267,81 +275,135 @@ router.get("/travel/api", travelController.api);
router.get("/travel/seed", ensureAuthenticated, travelController.seed);
// API routes cho quản lý FAQ sections (AJAX calls)
-router.post("/faq/api/add-section", ensureAuthenticated, 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);
+router.post(
+ "/faq/api/add-section",
+ ensureAuthenticated,
+ 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)
-router.put("/faq/api/update-sidebar", ensureAuthenticated, faqController.updateSidebarNav);
+router.put(
+ "/faq/api/update-sidebar",
+ ensureAuthenticated,
+ faqController.updateSidebarNav,
+);
// Safety routes
router.get("/safety", ensureAuthenticated, safetyController.index);
router.post("/safety/update", ensureAuthenticated, safetyController.update);
// Camp Location routes
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
router.get("/insurance", ensureAuthenticated, insuranceController.index);
-router.post("/insurance/update", ensureAuthenticated, insuranceController.update);
+router.post(
+ "/insurance/update",
+ ensureAuthenticated,
+ insuranceController.update,
+);
// Test Image Paths route
router.get("/test-images", ensureAuthenticated, (req, res) => {
- const fs = require('fs');
- const path = require('path');
- const campLocationData = require('../data/camp-location.json');
-
+ const fs = require("fs");
+ const path = require("path");
+ const campLocationData = require("../data/camp-location.json");
+
// Collect all image paths
const imagePaths = [];
-
+
// Camps images
if (campLocationData.camps) {
- campLocationData.camps.forEach(camp => {
+ campLocationData.camps.forEach((camp) => {
if (camp.image) {
imagePaths.push({
- type: 'Camp',
+ type: "Camp",
name: camp.title,
path: camp.image,
- exists: fs.existsSync(path.join(__dirname, '../public', camp.image))
+ exists: fs.existsSync(path.join(__dirname, "../public", camp.image)),
});
}
});
}
-
+
// Locations images
if (campLocationData.locations) {
- campLocationData.locations.forEach(location => {
+ campLocationData.locations.forEach((location) => {
if (location.imageSrc) {
imagePaths.push({
- type: 'Location',
+ type: "Location",
name: location.title,
path: location.imageSrc,
- exists: fs.existsSync(path.join(__dirname, '../public', location.imageSrc))
+ exists: fs.existsSync(
+ path.join(__dirname, "../public", location.imageSrc),
+ ),
});
}
-
+
// Program images
if (location.programOptions) {
- location.programOptions.forEach(program => {
+ location.programOptions.forEach((program) => {
if (program.imageSrc) {
imagePaths.push({
- type: 'Program',
+ type: "Program",
name: program.title,
path: program.imageSrc,
- exists: fs.existsSync(path.join(__dirname, '../public', program.imageSrc))
+ exists: fs.existsSync(
+ path.join(__dirname, "../public", program.imageSrc),
+ ),
});
}
});
}
});
}
-
- res.render('admin/test-images', {
- layout: 'layouts/admin',
- title: 'Test Image Paths',
+
+ res.render("admin/test-images", {
+ layout: "layouts/admin",
+ title: "Test Image Paths",
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;
diff --git a/routes/index.js b/routes/index.js
index beecd84..16ee43d 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -8,12 +8,12 @@ const headerController = require("../controllers/headerController");
const footerController = require("../controllers/footerController");
const contactController = require("../controllers/contactController");
const faqController = require("../controllers/faqController");
-
+const visaController = require("../controllers/visaController");
const safetyController = require("../controllers/safetyController");
const campLocationController = require("../controllers/campLocationController");
// Booking flow removed
-const insuranceController= require("../controllers/insuranceController");
+const insuranceController = require("../controllers/insuranceController");
const termsController = require("../controllers/termsController"); // <-- IMPORT ĐÃ CÓ
const activityController = require("../controllers/activityController");
const travelController = require("../controllers/travelController");
@@ -61,8 +61,7 @@ router.get("/api/activities/:id", activityController.apiDetail);
router.get("/api/camp-location", campLocationController.api);
// Booking routes removed
// Insurance APi route
-router.get("/api/insurance", insuranceController.api)
-
+router.get("/api/insurance", insuranceController.api);
router.get("/api/terms", termsController.api);
@@ -71,14 +70,14 @@ router.get("/travel", async (req, res) => {
try {
const Travel = require("../models/travel");
const travel = await Travel.findOne();
-
+
if (!travel) {
return res.status(404).render("errors/404", {
title: "Page Not Found",
message: "Travel information not found",
});
}
-
+
res.render("page/travel", {
title: travel.page.title,
data: travel.toObject(),
@@ -95,33 +94,55 @@ router.get("/api/travel", travelController.api);
// Booking submission APIs (public endpoints)
router.post("/api/booking/submit", bookingSubmissionController.submitBooking);
-router.get("/api/activity/:activityId/sessions", bookingSubmissionController.getAvailableSessions);
-router.get("/api/activity/:activityId/session/:sessionId/availability", bookingSubmissionController.getSessionAvailability);
+router.get(
+ "/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)
router.post(
"/api/camps/:program/sessions/:sessionId/bookings",
- activityController.createSessionBookingByProgram
+ activityController.createSessionBookingByProgram,
);
router.get(
"/api/camps/:program/sessions/:sessionId/bookings",
- activityController.getSessionBookingsByProgram
+ activityController.getSessionBookingsByProgram,
);
// Keep admin-style update/delete by activityId (protected) if needed
-router.put("/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", activityController.updateSessionBooking);
-router.delete("/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId", activityController.deleteSessionBooking);
+router.put(
+ "/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId",
+ activityController.updateSessionBooking,
+);
+router.delete(
+ "/api/camps/:activityId/sessions/:sessionId/bookings/:bookingId",
+ activityController.deleteSessionBooking,
+);
// Demo booking form
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
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
// 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;
diff --git a/scripts/2026_02_03_645124_visa.js b/scripts/2026_02_03_645124_visa.js
new file mode 100644
index 0000000..d3c031e
--- /dev/null
+++ b/scripts/2026_02_03_645124_visa.js
@@ -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();
diff --git a/server.js b/server.js
index 24d4a0c..cbd07b9 100644
--- a/server.js
+++ b/server.js
@@ -32,16 +32,7 @@ app.set("layout extractStyles", true);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
-app.use(
- "/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")),
-);
+app.use(express.static(path.join(__dirname, "public")));
// Session configuration
app.use(
diff --git a/views/admin/visa/index.ejs b/views/admin/visa/index.ejs
new file mode 100644
index 0000000..488d279
--- /dev/null
+++ b/views/admin/visa/index.ejs
@@ -0,0 +1,1231 @@
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Flag |
+ Country Name |
+ Services |
+ Actions |
+
+
+
+ <% if (data && data.hero && data.hero.summaryList) { %>
+ <% data.hero.summaryList.forEach(function(country) { %>
+
+ |
+ #<%= String(country.id).padStart(3, "0" ) %>
+ |
+
+
+ 
+
+ |
+
+ <%= country.name %>
+ |
+
+
+ <% country.services.forEach(function(service) { %>
+
+ <%= service %>
+
+ <% }); %>
+
+ |
+
+
+
+
+
+
+ |
+
+ <% }); %>
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/layouts/main.ejs b/views/layouts/main.ejs
index 3ec9a29..a715c0f 100644
--- a/views/layouts/main.ejs
+++ b/views/layouts/main.ejs
@@ -796,6 +796,13 @@
>Activity & Booking
+
+ Visa
+