forked from UKSOURCE/cms.hailearning.edu.vn
Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/thanh-02022026-news
This commit is contained in:
612
controllers/visaController.js
Normal file
612
controllers/visaController.js
Normal file
@@ -0,0 +1,612 @@
|
||||
// controllers/visaController.js
|
||||
|
||||
const addBaseUrlToImages = (data, baseUrl) => {
|
||||
if (!data) return data;
|
||||
|
||||
// Nếu là mảng, duyệt từng phần tử
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item) => addBaseUrlToImages(item, baseUrl));
|
||||
}
|
||||
|
||||
// Nếu là object, duyệt từng key
|
||||
if (typeof data === "object") {
|
||||
const newObj = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
// Kiểm tra nếu key là các trường chứa ảnh và value là string
|
||||
const imageKeys = ["icon", "mainImage", "bannerImage", "image"];
|
||||
|
||||
if (
|
||||
imageKeys.includes(key) &&
|
||||
typeof value === "string" &&
|
||||
!value.startsWith("http")
|
||||
) {
|
||||
newObj[key] = `${baseUrl}/${value}`
|
||||
.replace(/\/+/g, "/")
|
||||
.replace(":/", "://");
|
||||
}
|
||||
// Xử lý riêng cho mảng gallery (mảng các chuỗi)
|
||||
else if (key === "gallery" && Array.isArray(value)) {
|
||||
newObj[key] = value.map((img) =>
|
||||
img.startsWith("http")
|
||||
? img
|
||||
: `${baseUrl}/${img}`.replace(/\/+/g, "/").replace(":/", "://"),
|
||||
);
|
||||
}
|
||||
// Nếu là object hoặc mảng con khác, đệ quy tiếp
|
||||
else if (typeof value === "object" && value !== null) {
|
||||
newObj[key] = addBaseUrlToImages(value, baseUrl);
|
||||
} else {
|
||||
newObj[key] = value;
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
const Visa = require("../models/visa");
|
||||
const slugify = require("slugify");
|
||||
const createSlug = (text) => {
|
||||
return slugify(text, {
|
||||
lower: true, // Chuyển về chữ thường
|
||||
strict: true, // Loại bỏ ký tự đặc biệt
|
||||
locale: "vi", // Xử lý tiếng Việt chuẩn
|
||||
trim: true,
|
||||
});
|
||||
};
|
||||
// -------------------- Helper Functions --------------------
|
||||
|
||||
// Get visa data from MongoDB
|
||||
const getVisaData = async () => {
|
||||
const visa = await Visa.findOne().sort({ updatedAt: -1 }).lean();
|
||||
return visa || {};
|
||||
};
|
||||
|
||||
// Get default visa data structure (updated to match new JSON)
|
||||
const getDefaultVisaData = () => ({
|
||||
hero: {
|
||||
title: "Visa Service",
|
||||
summaryList: [],
|
||||
},
|
||||
});
|
||||
|
||||
// Helper function: Generate next country ID
|
||||
const getNextCountryId = (countries) => {
|
||||
if (!Array.isArray(countries) || countries.length === 0) return 1;
|
||||
return Math.max(...countries.map((c) => c.id || 0)) + 1;
|
||||
};
|
||||
|
||||
// -------------------- Admin Exports --------------------
|
||||
|
||||
// Display visa management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
// Fetch Visa data
|
||||
let data = await getVisaData();
|
||||
|
||||
// If no data exists, use default
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
data = getDefaultVisaData();
|
||||
} else {
|
||||
// Merge with defaults to ensure all fields exist
|
||||
const defaultData = getDefaultVisaData();
|
||||
|
||||
// Ensure hero section exists with defaults
|
||||
data.hero = data.hero || defaultData.hero;
|
||||
data.hero.title = data.hero.title || "Visa Service";
|
||||
data.hero.summaryList = data.hero.summaryList || [];
|
||||
}
|
||||
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
res.render("admin/visa/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Visa Management",
|
||||
data,
|
||||
frontendUrl,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
});
|
||||
// return res.json(data);
|
||||
} catch (err) {
|
||||
console.error("Visa index error:", err);
|
||||
req.flash("error_msg", "Error loading visa data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Get single country for edit
|
||||
exports.getCountry = async (req, res) => {
|
||||
console.log("--------------------------------------------------");
|
||||
console.log("🚀 [GET] Request nhận được tại /visa/edit/:id");
|
||||
|
||||
try {
|
||||
const { id } = req.params;
|
||||
console.log("📍 ID từ Params (URL):", id, "| Kiểu dữ liệu:", typeof id);
|
||||
|
||||
const visaData = await getVisaData();
|
||||
|
||||
// Kiểm tra cấu trúc dữ liệu tổng
|
||||
if (!visaData) {
|
||||
console.error("❌ Lỗi: Hàm getVisaData() trả về null/undefined");
|
||||
return res.status(404).json({ error: "Dữ liệu gốc không tồn tại" });
|
||||
}
|
||||
|
||||
if (!visaData.hero || !visaData.hero.summaryList) {
|
||||
console.error("❌ Lỗi: Cấu trúc visaData.hero.summaryList không hợp lệ");
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: "Không tìm thấy danh sách quốc gia" });
|
||||
}
|
||||
|
||||
console.log(
|
||||
"📊 Tổng số quốc gia hiện có trong mảng:",
|
||||
visaData.hero.summaryList.length,
|
||||
);
|
||||
|
||||
// 2. Tìm quốc gia theo ID
|
||||
const targetId = parseInt(id);
|
||||
console.log("🔍 Đang tìm kiếm Quốc gia có ID (sau khi parse):", targetId);
|
||||
|
||||
const country = visaData.hero.summaryList.find((c) => {
|
||||
// Log từng phần tử để kiểm tra kiểu dữ liệu của c.id trong DB
|
||||
// console.log(`Checking country: ${c.name} | ID in DB: ${c.id} (${typeof c.id})`);
|
||||
return c.id === targetId;
|
||||
});
|
||||
|
||||
if (!country) {
|
||||
console.warn(`⚠️ Không tìm thấy quốc gia nào khớp với ID: ${targetId}`);
|
||||
// In ra danh sách ID hiện có để so sánh
|
||||
const existingIds = visaData.hero.summaryList.map((c) => c.id);
|
||||
console.log("🆔 Các ID hiện có trong Database:", existingIds);
|
||||
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: `Không tìm thấy quốc gia có ID: ${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Tìm thấy dữ liệu quốc gia:", country.name);
|
||||
|
||||
// 3. Trả về dữ liệu
|
||||
res.json({
|
||||
success: true,
|
||||
country: country,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("🔴 Lỗi nghiêm trọng tại getCountry Controller:", err);
|
||||
res.status(500).json({ error: "Lỗi hệ thống khi tải thông tin quốc gia" });
|
||||
}
|
||||
};
|
||||
|
||||
// Update visa data (hero title only)
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
// Get current data
|
||||
const currentData = await getVisaData();
|
||||
|
||||
// Create updated data object
|
||||
const updatedData = {
|
||||
...(currentData.toObject ? currentData.toObject() : currentData),
|
||||
};
|
||||
|
||||
// Ensure hero structure exists
|
||||
updatedData.hero = updatedData.hero || {
|
||||
title: "Visa Service",
|
||||
summaryList: [],
|
||||
};
|
||||
|
||||
// Update hero title
|
||||
if (req.body.heroTitle) {
|
||||
updatedData.hero.title = req.body.heroTitle;
|
||||
}
|
||||
|
||||
// Check if there are changes
|
||||
const hasChanges =
|
||||
JSON.stringify(updatedData) !== JSON.stringify(currentData);
|
||||
|
||||
if (!hasChanges) {
|
||||
req.flash("info_msg", "No changes were made");
|
||||
return req.session.save(() => res.redirect("/admin/visa"));
|
||||
}
|
||||
|
||||
// Update or create document
|
||||
try {
|
||||
if (currentData._id) {
|
||||
await Visa.findByIdAndUpdate(currentData._id, updatedData, {
|
||||
new: true,
|
||||
});
|
||||
} else {
|
||||
await Visa.create(updatedData);
|
||||
}
|
||||
|
||||
req.flash("success_msg", "Visa data updated successfully");
|
||||
return req.session.save(() => res.redirect("/admin/visa"));
|
||||
} catch (dbError) {
|
||||
console.error("Database error:", dbError);
|
||||
req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`);
|
||||
return req.session.save(() => res.redirect("/admin/visa"));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Update error:", err);
|
||||
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
|
||||
return req.session.save(() => res.redirect("/admin/visa"));
|
||||
}
|
||||
};
|
||||
|
||||
// Add new country
|
||||
exports.addCountry = async (req, res) => {
|
||||
try {
|
||||
let visaData = await getVisaData();
|
||||
|
||||
// Initialize hero structure if not exist
|
||||
if (!visaData.hero || !visaData.hero.summaryList) {
|
||||
visaData = getDefaultVisaData();
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!req.body.name) {
|
||||
return res.status(400).json({ error: "Name is required" });
|
||||
}
|
||||
const finalSlug = req.body.slug
|
||||
? createSlug(req.body.slug)
|
||||
: createSlug(req.body.name);
|
||||
|
||||
// Parse services array
|
||||
let services = [];
|
||||
if (req.body.services) {
|
||||
if (typeof req.body.services === "string") {
|
||||
try {
|
||||
services = JSON.parse(req.body.services);
|
||||
} catch (e) {
|
||||
services = [req.body.services];
|
||||
}
|
||||
} else if (Array.isArray(req.body.services)) {
|
||||
services = req.body.services;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse detailedView if provided (optional)
|
||||
let detailedView = null;
|
||||
if (req.body.detailedView) {
|
||||
try {
|
||||
detailedView =
|
||||
typeof req.body.detailedView === "string"
|
||||
? JSON.parse(req.body.detailedView)
|
||||
: req.body.detailedView;
|
||||
} catch (e) {
|
||||
console.warn("Could not parse detailedView, creating without it");
|
||||
}
|
||||
}
|
||||
|
||||
// Create new country object
|
||||
const newCountry = {
|
||||
id: req.body.id || getNextCountryId(visaData.hero.summaryList),
|
||||
name: req.body.name,
|
||||
slug: finalSlug,
|
||||
icon: req.body.icon || "",
|
||||
services: services,
|
||||
...(detailedView && { detailedView }),
|
||||
};
|
||||
|
||||
// Add new country to summaryList
|
||||
visaData.hero.summaryList.push(newCountry);
|
||||
|
||||
// Update database
|
||||
const updatedData = {
|
||||
...(visaData.toObject ? visaData.toObject() : visaData),
|
||||
};
|
||||
|
||||
let savedData;
|
||||
if (visaData._id) {
|
||||
savedData = await Visa.findByIdAndUpdate(visaData._id, updatedData, {
|
||||
new: true,
|
||||
});
|
||||
} else {
|
||||
savedData = await Visa.create(updatedData);
|
||||
}
|
||||
|
||||
console.log(`✅ Country "${newCountry.name}" added successfully`);
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Country "${newCountry.name}" added successfully`,
|
||||
country: newCountry,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Add country error:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Update single country
|
||||
exports.updateCountry = async (req, res) => {
|
||||
try {
|
||||
// 1. Lấy ID từ params (URL)
|
||||
const { id } = req.params;
|
||||
let visaData = await getVisaData();
|
||||
|
||||
// if (!visaData || !visaData.hero || !visaData.hero.summaryList) {
|
||||
// return res
|
||||
// .status(400)
|
||||
// .json({ error: "Cấu trúc dữ liệu Visa không hợp lệ" });
|
||||
// }
|
||||
|
||||
// 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác)
|
||||
const countryIndex = visaData.hero.summaryList.findIndex(
|
||||
(c) => c.id === parseInt(id),
|
||||
);
|
||||
|
||||
if (countryIndex === -1) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: `Không tìm thấy quốc gia có ID: ${id}` });
|
||||
}
|
||||
|
||||
const currentCountry = visaData.hero.summaryList[countryIndex];
|
||||
let finalSlug = currentCountry.slug;
|
||||
if (req.body.name) {
|
||||
// Nếu name thay đổi, ta có thể cập nhật lại slug (tùy nhu cầu SEO)
|
||||
// Ở đây ưu tiên: Nếu có slug mới truyền lên thì dùng, không thì tạo từ name mới
|
||||
finalSlug = req.body.slug
|
||||
? createSlug(req.body.slug)
|
||||
: createSlug(req.body.name);
|
||||
}
|
||||
// 3. Xử lý dữ liệu từ req.body
|
||||
// Vì Client đã gửi JSON stringify, ta lấy trực tiếp hoặc parse nếu cần
|
||||
let services = req.body.services;
|
||||
if (typeof services === "string") {
|
||||
try {
|
||||
services = JSON.parse(services);
|
||||
} catch (e) {
|
||||
services = [services];
|
||||
}
|
||||
}
|
||||
|
||||
let detailedView = req.body.detailedView;
|
||||
if (typeof detailedView === "string") {
|
||||
try {
|
||||
detailedView = JSON.parse(detailedView);
|
||||
} catch (e) {
|
||||
detailedView = currentCountry.detailedView;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Cập nhật Object quốc gia
|
||||
const updatedCountry = {
|
||||
...currentCountry, // Giữ các trường cũ
|
||||
id: parseInt(id), // Đảm bảo ID không đổi
|
||||
name: req.body.name || currentCountry.name,
|
||||
slug: finalSlug,
|
||||
icon: req.body.icon || currentCountry.icon,
|
||||
services: Array.isArray(services) ? services : currentCountry.services,
|
||||
detailedView: detailedView || currentCountry.detailedView,
|
||||
};
|
||||
|
||||
// 5. Cập nhật vào mảng chính
|
||||
visaData.hero.summaryList[countryIndex] = updatedCountry;
|
||||
|
||||
// 6. Lưu vào Database
|
||||
if (visaData.markModified) {
|
||||
// Bắt buộc với Mongoose khi thay đổi nội dung bên trong Array/Object
|
||||
visaData.markModified("hero.summaryList");
|
||||
}
|
||||
|
||||
if (visaData._id) {
|
||||
await visaData.save(); // Sử dụng save() trực tiếp nếu visaData là Mongoose Document
|
||||
} else {
|
||||
await Visa.create(visaData);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Country "${updatedCountry.name}" updated successfully by ID: ${id}`,
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Quốc gia "${updatedCountry.name}" đã được cập nhật thành công`,
|
||||
country: updatedCountry,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Update country error:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Delete country
|
||||
exports.deleteCountry = async (req, res) => {
|
||||
try {
|
||||
const { slug } = req.params;
|
||||
let visaData = await getVisaData();
|
||||
|
||||
if (!visaData.hero || !visaData.hero.summaryList) {
|
||||
return res.status(400).json({ error: "Invalid visa data structure" });
|
||||
}
|
||||
|
||||
const countryIndex = visaData.hero.summaryList.findIndex(
|
||||
(c) => c.slug === slug,
|
||||
);
|
||||
|
||||
if (countryIndex === -1) {
|
||||
return res.status(404).json({ error: `Country "${slug}" not found` });
|
||||
}
|
||||
|
||||
const deletedCountry = visaData.hero.summaryList[countryIndex];
|
||||
visaData.hero.summaryList.splice(countryIndex, 1);
|
||||
|
||||
// Update database
|
||||
const updatedData = {
|
||||
...(visaData.toObject ? visaData.toObject() : visaData),
|
||||
};
|
||||
|
||||
if (visaData._id) {
|
||||
await Visa.findByIdAndUpdate(visaData._id, updatedData, { new: true });
|
||||
} else {
|
||||
await Visa.create(updatedData);
|
||||
}
|
||||
|
||||
console.log(`✅ Country "${deletedCountry.name}" deleted successfully`);
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Country "${deletedCountry.name}" deleted successfully`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Delete country error:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Public API Exports --------------------
|
||||
|
||||
// API to get all visa data for frontend
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const visaData = await getVisaData();
|
||||
if (!visaData) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Visa data not found",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
const heroData = visaData?.hero;
|
||||
|
||||
// 2. Lấy riêng phần hero (Dùng biến mới, không gán đè vào const)
|
||||
const baseUrl =
|
||||
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
|
||||
const processedData = addBaseUrlToImages(heroData, baseUrl);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
hero: processedData,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Visa API error:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading visa data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API to get all countries (summaryList only)
|
||||
exports.apiCountries = async (req, res) => {
|
||||
try {
|
||||
const visaData = await getVisaData();
|
||||
|
||||
if (!visaData || !visaData.hero || !visaData.hero.summaryList) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Countries data not found",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Lọc bỏ 'viewDetail' khỏi từng quốc gia trong danh sách
|
||||
const filteredCountries = visaData.hero.summaryList.map((item) => {
|
||||
// Tách detailedView ra, gom phần còn lại vào countryInfo
|
||||
const { detailedView, ...countryInfo } = item;
|
||||
|
||||
return {
|
||||
...countryInfo,
|
||||
// Lấy mainImage từ sâu bên trong detailedView và gán vào key mới
|
||||
mainImage: detailedView?.activeCountry?.mainImage || "",
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Gắn baseUrl vào ảnh cho danh sách đã lọc
|
||||
const processedData = filteredCountries;
|
||||
|
||||
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);
|
||||
|
||||
BIN
public/uploads/service/Learning.jpg
Normal file
BIN
public/uploads/service/Learning.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 464 KiB |
129
routes/admin.js
129
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");
|
||||
@@ -161,12 +161,12 @@ router.get(
|
||||
router.get(
|
||||
"/contact/submissions",
|
||||
ensureAuthenticated,
|
||||
contactController.getSubmissions
|
||||
contactController.getSubmissions,
|
||||
);
|
||||
router.put(
|
||||
"/contact/submissions/:id",
|
||||
ensureAuthenticated,
|
||||
contactController.updateSubmissionStatus
|
||||
contactController.updateSubmissionStatus,
|
||||
);
|
||||
|
||||
// Appointment management
|
||||
@@ -174,34 +174,46 @@ const appointmentController = require("../controllers/appointmentController");
|
||||
router.get(
|
||||
"/appointments",
|
||||
ensureAuthenticated,
|
||||
appointmentController.getAppointments
|
||||
appointmentController.getAppointments,
|
||||
);
|
||||
router.get(
|
||||
"/appointments/:id",
|
||||
ensureAuthenticated,
|
||||
appointmentController.getAppointmentById
|
||||
appointmentController.getAppointmentById,
|
||||
);
|
||||
router.put(
|
||||
"/appointments/:id",
|
||||
ensureAuthenticated,
|
||||
appointmentController.updateAppointmentStatus
|
||||
appointmentController.updateAppointmentStatus,
|
||||
);
|
||||
router.delete(
|
||||
"/appointments/:id",
|
||||
ensureAuthenticated,
|
||||
appointmentController.deleteAppointment
|
||||
appointmentController.deleteAppointment,
|
||||
);
|
||||
|
||||
// Appointment CMS page management
|
||||
router.get("/appointment", ensureAuthenticated, appointmentController.index);
|
||||
router.post("/appointment/update", ensureAuthenticated, appointmentController.update);
|
||||
router.get("/appointment/data", ensureAuthenticated, appointmentController.getAppointmentData);
|
||||
router.post(
|
||||
"/appointment/update",
|
||||
ensureAuthenticated,
|
||||
appointmentController.update,
|
||||
);
|
||||
router.get(
|
||||
"/appointment/data",
|
||||
ensureAuthenticated,
|
||||
appointmentController.getAppointmentData,
|
||||
);
|
||||
|
||||
// Pricing CMS page management
|
||||
const pricingController = require("../controllers/pricingController");
|
||||
router.get("/pricing", ensureAuthenticated, pricingController.index);
|
||||
router.post("/pricing/update", ensureAuthenticated, pricingController.update);
|
||||
router.get("/pricing/data", ensureAuthenticated, pricingController.getPricingData);
|
||||
router.get(
|
||||
"/pricing/data",
|
||||
ensureAuthenticated,
|
||||
pricingController.getPricingData,
|
||||
);
|
||||
|
||||
// Activity CRUD routes
|
||||
router.get("/activity", ensureAuthenticated, activityController.index);
|
||||
@@ -401,9 +413,9 @@ router.post(
|
||||
|
||||
// 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 = [];
|
||||
@@ -454,14 +466,39 @@ router.get("/test-images", ensureAuthenticated, (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
// Display visa management page
|
||||
router.get("/visa", ensureAuthenticated, visaController.index);
|
||||
|
||||
// Get country data for editing
|
||||
router.get("/visa/edit/:id", ensureAuthenticated, visaController.getCountry);
|
||||
|
||||
// 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/:id",
|
||||
ensureAuthenticated,
|
||||
visaController.updateCountry,
|
||||
);
|
||||
|
||||
// Delete country
|
||||
router.delete(
|
||||
"/delete/:slug",
|
||||
ensureAuthenticated,
|
||||
visaController.deleteCountry,
|
||||
);
|
||||
// Blog routes
|
||||
// Blog Management Routes
|
||||
router.get("/blog", ensureAuthenticated, blogController.index);
|
||||
@@ -477,21 +514,61 @@ router.post("/blog/:blogId/comments/:commentId/reject", ensureAuthenticated, blo
|
||||
router.post("/blog/:blogId/comments/:commentId/delete", ensureAuthenticated, blogController.deleteComment);
|
||||
|
||||
// Blog Categories Management
|
||||
router.get("/blog/categories", ensureAuthenticated, blogCategoryController.index);
|
||||
router.get("/blog/categories/create", ensureAuthenticated, blogCategoryController.create);
|
||||
router.post("/blog/categories/create", ensureAuthenticated, blogCategoryController.store);
|
||||
router.get("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.edit);
|
||||
router.post("/blog/categories/:id/edit", ensureAuthenticated, blogCategoryController.update);
|
||||
router.post("/blog/categories/:id/delete", ensureAuthenticated, blogCategoryController.destroy);
|
||||
router.post("/blog/categories/quick-create", ensureAuthenticated, blogCategoryController.quickCreate);
|
||||
router.get(
|
||||
"/blog/categories",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.index,
|
||||
);
|
||||
router.get(
|
||||
"/blog/categories/create",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.create,
|
||||
);
|
||||
router.post(
|
||||
"/blog/categories/create",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.store,
|
||||
);
|
||||
router.get(
|
||||
"/blog/categories/:id/edit",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.edit,
|
||||
);
|
||||
router.post(
|
||||
"/blog/categories/:id/edit",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.update,
|
||||
);
|
||||
router.post(
|
||||
"/blog/categories/:id/delete",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.destroy,
|
||||
);
|
||||
router.post(
|
||||
"/blog/categories/quick-create",
|
||||
ensureAuthenticated,
|
||||
blogCategoryController.quickCreate,
|
||||
);
|
||||
|
||||
// Blog Tags Management
|
||||
router.get("/blog/tags", ensureAuthenticated, blogTagController.index);
|
||||
router.get("/blog/tags/create", ensureAuthenticated, blogTagController.create);
|
||||
router.post("/blog/tags/create", ensureAuthenticated, blogTagController.store);
|
||||
router.get("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.edit);
|
||||
router.post("/blog/tags/:id/edit", ensureAuthenticated, blogTagController.update);
|
||||
router.post("/blog/tags/:id/delete", ensureAuthenticated, blogTagController.destroy);
|
||||
router.post("/blog/tags/quick-create", ensureAuthenticated, blogTagController.quickCreate);
|
||||
router.post(
|
||||
"/blog/tags/:id/edit",
|
||||
ensureAuthenticated,
|
||||
blogTagController.update,
|
||||
);
|
||||
router.post(
|
||||
"/blog/tags/:id/delete",
|
||||
ensureAuthenticated,
|
||||
blogTagController.destroy,
|
||||
);
|
||||
router.post(
|
||||
"/blog/tags/quick-create",
|
||||
ensureAuthenticated,
|
||||
blogTagController.quickCreate,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
@@ -190,4 +190,7 @@ router.get("/api/service/:slug", serviceController.getServiceBySlug);
|
||||
// Service slugs list
|
||||
router.get("/api/service-slugs", serviceController.getServiceSlugs);
|
||||
|
||||
router.get("/api/visa", visaController.api);
|
||||
router.get("/api/visa/country", visaController.apiCountries);
|
||||
|
||||
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();
|
||||
@@ -55,9 +55,7 @@ app.use(
|
||||
);
|
||||
|
||||
// Serve other public files
|
||||
app.use(
|
||||
express.static(path.join(__dirname, "public")),
|
||||
);
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
// Session configuration (using MongoDB store to avoid logout khi server restart)
|
||||
app.use(
|
||||
|
||||
@@ -2,7 +2,6 @@ const Service = require("../models/service");
|
||||
|
||||
const getServiceData = async () => {
|
||||
const service = await Service.findOne().sort({ updatedAt: -1 });
|
||||
console.log("check layout", service.services.items.layout);
|
||||
|
||||
if (!service) {
|
||||
return {
|
||||
|
||||
@@ -249,9 +249,9 @@
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID</label>
|
||||
<label class="form-label">ID (Auto-generated)</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="<%= faq.id || 'faq-' + (index + 1) %>" required>
|
||||
value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
@@ -399,20 +399,22 @@ function removeFeature(button) {
|
||||
|
||||
function addFAQ() {
|
||||
const container = document.getElementById('faqContainer');
|
||||
const newFaqId = generateFAQId();
|
||||
const faqNumber = document.querySelectorAll('.faq-item').length + 1;
|
||||
const faqHtml = `
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">FAQ ${faqIndex + 1}</h6>
|
||||
<h6 class="mb-0 text-decoration-underline">FAQ ${faqNumber}</h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID</label>
|
||||
<label class="form-label">ID (Auto-generated)</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="faq-${faqIndex + 1}" required>
|
||||
value="${newFaqId}" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
@@ -441,13 +443,43 @@ function addFAQ() {
|
||||
faqIndex++;
|
||||
}
|
||||
|
||||
function updateFAQId(questionInput) {
|
||||
// Không cần update ID nữa vì đã tự động theo số thứ tự
|
||||
}
|
||||
|
||||
function removeFAQ(button) {
|
||||
const faqItem = button.closest('.faq-item');
|
||||
if (faqItem) {
|
||||
faqItem.remove();
|
||||
// Cập nhật lại số thứ tự và ID của tất cả FAQ
|
||||
updateFAQNumbers();
|
||||
}
|
||||
}
|
||||
|
||||
function generateFAQId() {
|
||||
// Đếm số lượng FAQ hiện tại và tạo ID tiếp theo
|
||||
const existingFAQs = document.querySelectorAll('.faq-item');
|
||||
const nextNumber = existingFAQs.length + 1;
|
||||
return `faq-${nextNumber}`;
|
||||
}
|
||||
|
||||
function updateFAQNumbers() {
|
||||
// Cập nhật lại tất cả FAQ ID và số thứ tự
|
||||
const faqItems = document.querySelectorAll('.faq-item');
|
||||
faqItems.forEach((item, index) => {
|
||||
const number = index + 1;
|
||||
const idInput = item.querySelector('.faq-id');
|
||||
const titleElement = item.querySelector('h6');
|
||||
|
||||
if (idInput) {
|
||||
idInput.value = `faq-${number}`;
|
||||
}
|
||||
if (titleElement) {
|
||||
titleElement.textContent = `FAQ ${number}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
// Collect basic details data
|
||||
const details = {
|
||||
|
||||
@@ -283,6 +283,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Delete Confirmation Modal -->
|
||||
<div id="customDeleteModal" class="custom-modal" style="display: none;">
|
||||
<div class="custom-modal-backdrop"></div>
|
||||
<div class="custom-modal-dialog">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">
|
||||
<i class="fas fa-exclamation-triangle text-warning me-2"></i>Confirm Delete
|
||||
</h5>
|
||||
<button type="button" class="custom-modal-close" onclick="closeDeleteModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<div class="text-center mb-3">
|
||||
<div class="delete-icon">
|
||||
<i class="fas fa-trash fa-2x text-danger"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h6 class="text-center mb-3">Delete Service</h6>
|
||||
<p class="text-center text-muted mb-0">
|
||||
Are you sure you want to delete "<strong id="deleteServiceName"></strong>"?
|
||||
<br><small class="text-danger">This action cannot be undone.</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="fas fa-trash me-2"></i>Delete Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
@@ -534,11 +571,110 @@ function addService() {
|
||||
}
|
||||
|
||||
function deleteService(index) {
|
||||
if (confirm('Are you sure you want to delete this service? This action cannot be undone.')) {
|
||||
const service = servicesData[index];
|
||||
if (!service) return;
|
||||
|
||||
// Set service name in modal
|
||||
document.getElementById('deleteServiceName').textContent = service.name || 'Unnamed Service';
|
||||
|
||||
// Show custom modal
|
||||
showDeleteModal();
|
||||
|
||||
// Handle confirm delete
|
||||
const confirmBtn = document.getElementById('confirmDeleteBtn');
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
|
||||
newConfirmBtn.addEventListener('click', async function() {
|
||||
// Show loading state
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Deleting...';
|
||||
|
||||
// Disable cancel button and close button during delete
|
||||
const cancelBtn = document.querySelector('.custom-modal .btn-secondary');
|
||||
const closeBtn = document.querySelector('.custom-modal-close');
|
||||
cancelBtn.disabled = true;
|
||||
closeBtn.disabled = true;
|
||||
|
||||
// Add loading overlay to modal
|
||||
showModalLoading();
|
||||
|
||||
try {
|
||||
// Simulate API call delay for better UX
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Perform delete
|
||||
servicesData.splice(index, 1);
|
||||
updateServicesTable();
|
||||
updateStatistics();
|
||||
showSuccess('Service deleted successfully!');
|
||||
|
||||
// Hide modal
|
||||
closeDeleteModal();
|
||||
|
||||
// Show success message
|
||||
showSuccess(`Service "${service.name}" deleted successfully!`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting service:', error);
|
||||
showError('Failed to delete service. Please try again.');
|
||||
} finally {
|
||||
// Reset button states
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
|
||||
cancelBtn.disabled = false;
|
||||
closeBtn.disabled = false;
|
||||
|
||||
// Hide loading overlay
|
||||
hideModalLoading();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showModalLoading() {
|
||||
const modal = document.getElementById('customDeleteModal');
|
||||
const loadingOverlay = document.createElement('div');
|
||||
loadingOverlay.className = 'modal-loading-overlay';
|
||||
loadingOverlay.innerHTML = `
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-primary"></i>
|
||||
<p class="mt-2 mb-0">Deleting service...</p>
|
||||
</div>
|
||||
`;
|
||||
modal.appendChild(loadingOverlay);
|
||||
}
|
||||
|
||||
function hideModalLoading() {
|
||||
const loadingOverlay = document.querySelector('.modal-loading-overlay');
|
||||
if (loadingOverlay) {
|
||||
loadingOverlay.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function showDeleteModal() {
|
||||
const modal = document.getElementById('customDeleteModal');
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add animation
|
||||
setTimeout(() => {
|
||||
modal.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
// Close modal when clicking backdrop (only if not loading)
|
||||
const backdrop = modal.querySelector('.custom-modal-backdrop');
|
||||
backdrop.onclick = function() {
|
||||
if (!document.querySelector('.modal-loading-overlay')) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal with ESC key (only if not loading)
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
}
|
||||
|
||||
function handleEscKey(event) {
|
||||
if (event.key === 'Escape' && !document.querySelector('.modal-loading-overlay')) {
|
||||
closeDeleteModal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,4 +915,188 @@ function showError(message) {
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* Custom Delete Modal Styles */
|
||||
.custom-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-modal.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.custom-modal-backdrop {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.custom-modal-dialog {
|
||||
position: relative;
|
||||
z-index: 10001;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
transform: scale(0.8);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-modal.show .custom-modal-dialog {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||
padding: 20px 25px 15px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
color: var(--primary-dark);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.custom-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
color: #6c757d;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-modal-close:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
padding: 15px 25px 25px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-modal-footer .btn {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.custom-modal .btn-danger {
|
||||
background: linear-gradient(135deg, #dc3545, #c82333);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-modal .btn-danger:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.custom-modal .btn-secondary {
|
||||
background: #6c757d;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-modal .btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Modal Loading Overlay */
|
||||
.modal-loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10002;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.loading-spinner p {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Disable buttons during loading */
|
||||
.custom-modal .btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.custom-modal-close:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
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
@@ -722,11 +722,10 @@
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/about' || currentPath === '/admin/affiliations' || currentPath === '/admin/partnerships' ? 'active' : '' %>"
|
||||
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/about-us' || currentPath === '/admin/safety' || currentPath === '/admin/faq' || currentPath === '/admin/insurance' || currentPath === '/admin/travel' || currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
|
||||
href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
About
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
|
||||
@@ -737,13 +736,13 @@
|
||||
href="/admin/safety">Safety</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>" href="/admin/faq">FAQ</a>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/faq' ? 'active' : '' %>"
|
||||
href="/admin/faq">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>"
|
||||
href="/admin/insurance">Insurance</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
|
||||
href="/admin/travel">Travel</a>
|
||||
@@ -752,12 +751,27 @@
|
||||
<a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
|
||||
href="/admin/terms-conditions">Terms & Conditions</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/service' || currentPath === '/admin/activity' ? 'active' : '' %>"
|
||||
href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Services
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
|
||||
href="/admin/service">Service</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/activity' ? 'active' : '' %>"
|
||||
href="/admin/activity">Activity & Booking</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
|
||||
Us</a>
|
||||
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
|
||||
href="/admin/contact">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>"
|
||||
@@ -771,42 +785,6 @@
|
||||
<a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
|
||||
href="/admin/camp-location">Camp Location</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>" href="/admin/activity">Activity
|
||||
& Booking</a>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
|
||||
href="/admin/travel">Travel</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
|
||||
href="/admin/terms-conditions">Terms & Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
|
||||
href="/admin/service">Service</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
|
||||
Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
|
||||
href="/admin/camp-location">Camp Location</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>" href="/admin/activity">Activity &
|
||||
Booking</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<% if (locals.user) { %>
|
||||
|
||||
Reference in New Issue
Block a user