first commit

This commit is contained in:
2026-04-11 14:08:27 +07:00
parent e86e5d2c46
commit 6b7655aa16
389 changed files with 5387 additions and 60861 deletions

View File

@@ -1,108 +0,0 @@
const mongoose = require("mongoose");
const aboutUsSchema = new mongoose.Schema(
{
hero: {
title: String,
breadcrumb: [String],
backgroundImage: String,
},
intro: {
subheading: String,
heading: String,
description: String,
image: String,
},
mission: {
subheading: String,
heading: String,
description: String,
images: {
main: String,
secondary: String,
bgShape: String,
planeShape: String,
topShape: String,
globeShape: String,
},
items: [
new mongoose.Schema(
{
icon: String,
label: String,
description: String,
},
{ _id: false },
),
],
features: [String],
ctaButton: {
label: String,
href: String,
},
},
features: {
backgroundImage: String,
subheading: String,
heading: String,
description: String,
image: String,
items: [
new mongoose.Schema(
{
icon: String,
title: String,
description: String,
},
{ _id: false },
),
],
ctaButton: {
label: String,
href: String,
},
},
news: {
subheading: String,
heading: String,
ctaButton: {
label: String,
href: String,
},
selectedBlogIds: [{ type: mongoose.Schema.Types.ObjectId, ref: "Blog" }],
// Deprecated: items field kept for backward compatibility during migration
items: [
new mongoose.Schema(
{
title: String,
category: String,
date: String,
comments: Number,
author: {
name: String,
avatar: String,
},
link: String,
thumbnail: String,
},
{ _id: false },
),
],
},
},
{
timestamps: true,
collection: "aboutus",
},
);
// Static method để đảm bảo luôn chỉ có 1 bản ghi duy nhất (Singleton)
aboutUsSchema.statics.getSingle = async function () {
let doc = await this.findOne();
if (!doc) {
doc = await this.create({});
}
return doc;
};
module.exports = mongoose.model("AboutUs", aboutUsSchema);

View File

@@ -1,194 +0,0 @@
const mongoose = require("mongoose");
const activitySchema = new mongoose.Schema(
{
// Hero section for activity page header (supports Activities and Booking variants)
hero: {
titleActivities: {
type: String,
trim: true,
default: ''
},
titleBooking: {
type: String,
trim: true,
default: ''
},
bannerImageActivities: {
type: String,
trim: true,
default: ''
},
bannerImageBooking: {
type: String,
trim: true,
default: ''
},
},
name: {
type: String,
required: true,
trim: true,
},
price: {
type: Number,
required: true,
min: 0,
},
priceText: {
type: String,
trim: true,
},
season: [
{
type: String,
enum: ["spring", "summer", "autumn", "winter"],
},
],
age: {
type: [Number],
validate: {
validator: function (v) {
return v.length === 2 && v[0] <= v[1];
},
message: "Age must be an array of [minAge, maxAge]",
},
},
locations: [
{
type: String,
trim: true,
},
],
image: {
type: String,
trim: true,
},
link: {
type: String,
trim: true,
},
// Global filters document (single document in Activity collection)
filters: [
{
label: { type: String, required: true, trim: true },
value: { type: String, required: true, trim: true },
items: [
{
value: { type: String, required: true },
label: { type: String, required: true },
},
],
order: { type: Number, default: 0 },
},
],
program: {
type: String,
trim: true,
},
rating: {
type: Number,
min: 1,
max: 5,
default: 4,
},
isActive: {
type: Boolean,
default: true,
},
order: {
type: Number,
default: 0,
},
// marker for the single document that stores global filters
isFiltersDoc: {
type: Boolean,
default: false,
},
// Rich camp details from camp-detail field in activities.json
campDetail: {
type: mongoose.Schema.Types.Mixed,
default: {},
},
// Booking sessions - các đợt booking với thông số riêng
bookingSessions: [
{
sessionId: { type: String, required: true },
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
overnightStays: { type: Number, required: true, default: 14 },
// Spots theo giới tính
totalMaleSpots: { type: Number, default: 25 },
totalFemaleSpots: { type: Number, default: 25 },
bookedMaleSpots: { type: Number, default: 0 },
bookedFemaleSpots: { type: Number, default: 0 },
price: { type: Number },
isActive: { type: Boolean, default: true },
// Danh sách booking cho session này
bookingList: [
{
address: { type: String, required: true },
agreeNewsletter: { type: Boolean, default: false },
agreeTerms: { type: Boolean, required: true },
city: { type: String, required: true },
country: { type: String, required: true },
dietaryRestrictions: {
type: String,
enum: ['none', 'vegetarian', 'vegan', 'halal', 'kosher', 'gluten-free', 'other'],
default: 'none'
},
email: {
type: String,
required: true,
lowercase: true,
trim: true
},
emergencyContact: { type: String, required: true },
emergencyPhone: { type: String, required: true },
medicalConditions: { type: String, default: '' },
numberOfParticipants: { type: Number, required: true, min: 1 },
parentFirstName: { type: String, required: true, trim: true },
parentLastName: { type: String, required: true, trim: true },
participantBirthDate: { type: Date, required: true },
participantFirstName: { type: String, required: true, trim: true },
participantGender: {
type: String,
enum: ['male', 'female', 'other'],
required: true
},
participantLastName: { type: String, required: true, trim: true },
phone: { type: String, required: true },
postalCode: { type: String, required: true },
sessionDate: { type: String, required: true }, // sessionId reference
specialRequests: { type: String, default: '' },
// Thêm các trường quản lý
bookingStatus: {
type: String,
enum: ['pending', 'confirmed', 'cancelled', 'completed'],
default: 'pending'
},
paymentStatus: {
type: String,
enum: ['pending', 'partial', 'paid', 'refunded'],
default: 'pending'
},
totalAmount: { type: Number, default: 0 },
paidAmount: { type: Number, default: 0 },
bookingDate: { type: Date, default: Date.now },
confirmationCode: { type: String, unique: true },
adminNotes: { type: String, default: '' }
}
]
}
],
},
{timestamps: true}
);
// Add index for better query performance
activitySchema.index({name: 1});
activitySchema.index({isActive: 1, order: 1});
activitySchema.index({season: 1});
activitySchema.index({locations: 1});
module.exports = mongoose.model("Activity", activitySchema);

View File

@@ -1,206 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Appointment) {
delete mongoose.models.Appointment;
}
if (mongoose.connection.models.Appointment) {
delete mongoose.connection.models.Appointment;
}
// Schema cho hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Make Appointment",
},
backgroundImage: {
type: String,
trim: true,
default: "",
},
subtitle: {
type: String,
trim: true,
default: "",
},
heading: {
type: String,
trim: true,
default: "",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema cho form field
const formFieldSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
label: {
type: String,
trim: true,
default: "",
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "date", "select"],
},
placeholder: {
type: String,
trim: true,
default: "",
},
required: {
type: Boolean,
default: false,
},
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
},
{ _id: false }
);
// Schema cho submit button
const submitButtonSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
default: "Request Appointment",
},
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn",
},
},
{ _id: false }
);
// Schema cho form
const formSchema = new mongoose.Schema(
{
heading: {
type: String,
trim: true,
default: "Request Appointment",
},
fields: {
type: [formFieldSchema],
default: [],
},
submitButton: {
type: submitButtonSchema,
default: () => ({}),
},
},
{ _id: false }
);
// Main Appointment Schema
const appointmentSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
visaOptions: {
type: [String],
default: [],
},
form: {
type: formSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
appointmentSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default appointment exists
const existingAppointment = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Make Appointment",
backgroundImage: jsonData.hero?.backgroundImage || "",
subtitle: jsonData.hero?.subtitle || "",
heading: jsonData.hero?.heading || "",
description: jsonData.hero?.description || "",
},
visaOptions: Array.isArray(jsonData.visaOptions) ? jsonData.visaOptions : [],
form: {
heading: jsonData.form?.heading || "Request Appointment",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
label: field.label || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
colClass: field.colClass || "col-lg-12",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Request Appointment",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn",
},
},
};
if (existingAppointment) {
// Update existing appointment
existingAppointment.hero = processedData.hero;
existingAppointment.visaOptions = processedData.visaOptions;
existingAppointment.form = processedData.form;
await existingAppointment.save();
console.log("Appointment data updated successfully");
return existingAppointment;
} else {
// Create new appointment
const newAppointment = await this.create({
name: "default",
...processedData,
});
console.log("Appointment data imported successfully");
return newAppointment;
}
} catch (error) {
console.error("Error migrating appointment data:", error);
throw error;
}
};
module.exports = mongoose.model("Appointment", appointmentSchema);

View File

@@ -1,83 +0,0 @@
const mongoose = require("mongoose");
/**
* Schema for Appointment Submissions
* Stores appointment requests from users
*/
const appointmentSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
appointmentDate: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
visaTypes: {
type: [String],
default: [],
},
status: {
type: String,
enum: ["pending", "confirmed", "completed", "cancelled"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
confirmedAt: {
type: Date,
default: null,
},
completedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
appointmentSubmissionSchema.index({ status: 1, createdAt: -1 });
appointmentSubmissionSchema.index({ email: 1 });
appointmentSubmissionSchema.index({ appointmentDate: 1 });
module.exports = mongoose.model("AppointmentSubmission", appointmentSubmissionSchema);

View File

@@ -1,148 +0,0 @@
const mongoose = require('mongoose');
const blogSchema = new mongoose.Schema({
// Basic blog information
title: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
unique: true,
trim: true
},
excerpt: {
type: String,
required: true,
maxlength: 500
},
content: {
type: mongoose.Schema.Types.Mixed, // Có thể là string HTML hoặc JSON EditorJS
required: true
},
// Media
featuredImage: {
type: String,
default: ''
},
galleryImages: [{
type: String
}], // Mảng URL ảnh cho gallery (details-2, details-3)
// Author and publishing
author: {
type: String,
default: 'Admin'
},
publishedAt: {
type: String, // Format: "11 March 2025"
default: function() {
return new Date().toLocaleDateString('en-GB', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
}
},
// Categorization (simple strings, no references)
category: [{
type: String // ["Visa", "Travel", ...] - Một bài có thể thuộc nhiều category
}],
tags: [{
type: String // ["WorkVisa", "StudentVisa", ...]
}],
// Status and features
status: {
type: String,
enum: ['draft', 'published'],
default: 'published'
},
isFeatured: {
type: Boolean,
default: false
},
// Comments count (có thể fake trước)
commentsCount: {
type: Number,
default: 0
},
// Quote/Sidebar section
quote: {
type: String,
default: '',
trim: true
},
// Content after quote
contentAfterQuote: {
type: String,
default: '',
trim: true
}
}, {
timestamps: true
});
// Indexes
blogSchema.index({ status: 1, createdAt: -1 });
blogSchema.index({ category: 1, status: 1 });
blogSchema.index({ isFeatured: 1, status: 1 });
blogSchema.index({ tags: 1, status: 1 });
// Remove __v from JSON output
blogSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Pre-save middleware
blogSchema.pre('save', function(next) {
// Auto-generate slug if not provided
if (!this.slug && this.title) {
this.slug = this.title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
}
next();
});
// Static methods
blogSchema.statics.getPublished = function() {
return this.find({ status: 'published' }).sort({ createdAt: -1 });
};
blogSchema.statics.getFeatured = function() {
return this.find({
status: 'published',
isFeatured: true
}).sort({ createdAt: -1 });
};
blogSchema.statics.getByCategory = function(category) {
return this.find({
status: 'published',
category: { $in: [category] } // Tìm trong array categories
}).sort({ createdAt: -1 });
};
blogSchema.statics.getByTag = function(tag) {
return this.find({
status: 'published',
tags: tag
}).sort({ createdAt: -1 });
};
module.exports = mongoose.model('Blog', blogSchema);

View File

@@ -1,75 +0,0 @@
const mongoose = require('mongoose');
const blogCategorySchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
unique: true // "Permanent Residency (PR)"
},
slug: {
type: String,
required: true,
unique: true,
trim: true // "permanent-residency"
},
postCount: {
type: Number,
default: 0 // "(04)"
},
description: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
// Indexes
blogCategorySchema.index({ isActive: 1, name: 1 });
// Remove __v from JSON output
blogCategorySchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Pre-save middleware
blogCategorySchema.pre('save', function(next) {
// Auto-generate slug if not provided
if (!this.slug && this.name) {
this.slug = this.name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
}
next();
});
// Static methods
blogCategorySchema.statics.getActive = function() {
return this.find({ isActive: true }).sort({ name: 1 });
};
// Method to update post count
blogCategorySchema.methods.updatePostCount = async function() {
const Blog = require('./blog');
const count = await Blog.countDocuments({
category: { $in: [this.name] }, // Tìm trong array categories
status: 'published'
});
this.postCount = count;
await this.save();
return count;
};
module.exports = mongoose.model('BlogCategory', blogCategorySchema);

View File

@@ -1,104 +0,0 @@
const mongoose = require('mongoose');
const blogCommentSchema = new mongoose.Schema({
postId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Blog',
required: true
},
authorName: {
type: String,
required: true,
trim: true // "Frank Flores"
},
authorEmail: {
type: String,
default: '',
trim: true
},
authorPhone: {
type: String,
default: '',
trim: true
},
authorAddress: {
type: String,
default: '',
trim: true
},
authorDate: {
type: String,
default: '',
trim: true
},
authorAvatar: {
type: String,
default: '' // "/assets/img/inner-page/news-details/comment-1.png"
},
content: {
type: String,
required: true,
trim: true
},
parentId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'BlogComment',
default: null // Cho threaded comments (reply)
},
status: {
type: String,
enum: ['pending', 'approved', 'rejected'],
default: 'pending'
}
}, {
timestamps: true // Vẫn giữ timestamps cho admin quản lý
});
// Indexes
blogCommentSchema.index({ postId: 1, status: 1, createdAt: -1 });
blogCommentSchema.index({ parentId: 1 });
blogCommentSchema.index({ status: 1 });
// Remove __v from JSON output
blogCommentSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Static methods
blogCommentSchema.statics.getApprovedByPost = function(postId) {
return this.find({
postId: postId,
status: 'approved',
parentId: null // Chỉ lấy comments gốc, không lấy replies
}).sort({ createdAt: -1 });
};
blogCommentSchema.statics.getReplies = function(parentId) {
return this.find({
parentId: parentId,
status: 'approved'
}).sort({ createdAt: 1 }); // Replies sắp xếp theo thời gian tăng dần
};
blogCommentSchema.statics.getByStatus = function(status) {
return this.find({ status: status })
.populate('postId', 'title slug')
.sort({ createdAt: -1 });
};
// Method to approve comment
blogCommentSchema.methods.approve = function() {
this.status = 'approved';
return this.save();
};
// Method to reject comment
blogCommentSchema.methods.reject = function() {
this.status = 'rejected';
return this.save();
};
module.exports = mongoose.model('BlogComment', blogCommentSchema);

View File

@@ -1,77 +0,0 @@
const mongoose = require('mongoose');
const blogTagSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
unique: true // "WorkVisa"
},
slug: {
type: String,
required: true,
unique: true,
trim: true // "work-visa"
},
postCount: {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
// Indexes
blogTagSchema.index({ isActive: 1, name: 1 });
// Remove __v from JSON output
blogTagSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Pre-save middleware
blogTagSchema.pre('save', function(next) {
// Auto-generate slug if not provided
if (!this.slug && this.name) {
this.slug = this.name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim('-');
}
next();
});
// Static methods
blogTagSchema.statics.getActive = function() {
return this.find({ isActive: true }).sort({ name: 1 });
};
blogTagSchema.statics.getPopular = function(limit = 10) {
return this.find({ isActive: true })
.sort({ postCount: -1, name: 1 })
.limit(limit);
};
// Method to update post count
blogTagSchema.methods.updatePostCount = async function() {
const Blog = require('./blog');
const count = await Blog.countDocuments({
tags: { $in: [this.name] }, // Tìm trong array tags
status: 'published'
});
this.postCount = count;
await this.save();
return count;
};
module.exports = mongoose.model('BlogTag', blogTagSchema);

View File

@@ -1,106 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Booking) {
delete mongoose.models.Booking;
}
if (mongoose.connection.models.Booking) {
delete mongoose.connection.models.Booking;
}
const bookingSchema = new mongoose.Schema(
{
hero: {
title: String,
backgroundImage: String,
},
searchBar: {
locationLabel: String,
holidaySeasonLabel: String,
searchButtonText: String,
},
filterPanel: {
title: String,
priceTitle: String,
priceLabel: String,
pricePlaceholder: String,
priceMin: Number,
priceMax: Number,
activitiesTitle: String,
ageTitle: String,
ageSelectPlaceholder: String,
ageMin: Number,
ageMax: Number,
ratingTitle: String,
ratingOptions: [
{
value: String,
label: String,
},
],
resetButtonText: String,
},
programs: [
{
value: String,
label: String,
},
],
holidays: [
{
value: String,
label: String,
},
],
locations: [
{
value: String,
label: String,
},
],
camps: [
{
name: String,
price: Number,
priceText: String,
season: [String],
age: [Number],
locations: [String],
image: String,
link: String,
program: String,
rating: Number,
},
],
// Configuration - Dùng Mixed type để chấp nhận bất kỳ structure nào
configuration: mongoose.Schema.Types.Mixed,
formSteps: [
{
step: Number,
title: String,
sections: [
{
id: String,
fields: [mongoose.Schema.Types.Mixed],
},
],
},
],
validation: mongoose.Schema.Types.Mixed,
},
{
timestamps: true,
strict: false
}
);
module.exports = mongoose.model("Booking", bookingSchema);

View File

@@ -1,200 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.BookingSubmission) {
delete mongoose.models.BookingSubmission;
}
if (mongoose.connection.models.BookingSubmission) {
delete mongoose.connection.models.BookingSubmission;
}
const bookingSubmissionSchema = new mongoose.Schema(
{
// Liên kết với activity và session
activityId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Activity',
required: true
},
sessionId: {
type: String,
required: true
},
// Thông tin người đăng ký
parentFirstName: {
type: String,
required: true,
trim: true
},
parentLastName: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true,
lowercase: true
},
phone: {
type: String,
required: true,
trim: true
},
// Thông tin địa chỉ
address: {
type: String,
required: true,
trim: true
},
city: {
type: String,
required: true,
trim: true
},
country: {
type: String,
required: true,
trim: true
},
postalCode: {
type: String,
required: true,
trim: true
},
// Thông tin người tham gia
participantFirstName: {
type: String,
required: true,
trim: true
},
participantLastName: {
type: String,
required: true,
trim: true
},
participantBirthDate: {
type: Date,
required: true
},
participantGender: {
type: String,
enum: ['male', 'female', 'other'],
required: true
},
numberOfParticipants: {
type: Number,
required: true,
min: 1
},
// Thông tin y tế và đặc biệt
medicalConditions: {
type: String,
trim: true,
default: ''
},
dietaryRestrictions: {
type: String,
enum: ['none', 'vegetarian', 'vegan', 'halal', 'kosher', 'gluten-free', 'other'],
default: 'none'
},
specialRequests: {
type: String,
trim: true,
default: ''
},
// Thông tin liên hệ khẩn cấp
emergencyContact: {
type: String,
required: true,
trim: true
},
emergencyPhone: {
type: String,
required: true,
trim: true
},
// Điều khoản và thông báo
agreeTerms: {
type: Boolean,
required: true,
default: false
},
agreeNewsletter: {
type: Boolean,
default: false
},
// Trạng thái đăng ký
status: {
type: String,
enum: ['pending', 'confirmed', 'cancelled', 'completed'],
default: 'pending'
},
// Ghi chú admin
adminNotes: {
type: String,
trim: true,
default: ''
},
// Thông tin thanh toán
paymentStatus: {
type: String,
enum: ['pending', 'partial', 'paid', 'refunded'],
default: 'pending'
},
totalAmount: {
type: Number,
default: 0
},
paidAmount: {
type: Number,
default: 0
}
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
// Virtual để tính tuổi của participant
bookingSubmissionSchema.virtual('participantAge').get(function() {
if (this.participantBirthDate) {
const today = new Date();
const birthDate = new Date(this.participantBirthDate);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
return 0;
});
// Virtual để lấy thông tin activity
bookingSubmissionSchema.virtual('activity', {
ref: 'Activity',
localField: 'activityId',
foreignField: '_id',
justOne: true
});
// Index for better performance
bookingSubmissionSchema.index({ activityId: 1, sessionId: 1 });
bookingSubmissionSchema.index({ email: 1 });
bookingSubmissionSchema.index({ status: 1 });
bookingSubmissionSchema.index({ createdAt: -1 });
module.exports = mongoose.model("BookingSubmission", bookingSubmissionSchema);

32
models/certificate.js Normal file
View File

@@ -0,0 +1,32 @@
const mongoose = require('mongoose');
const certificateSchema = new mongoose.Schema({
certification_number: {
type: String, required: true, unique: true, trim: true
},
student_name: {
type: String, required: true, trim: true
},
program_name: {
type: String, required: true, trim: true
},
department: {
type: mongoose.Schema.Types.ObjectId, ref: 'Department', required: true
},
level: {
type: mongoose.Schema.Types.ObjectId, ref: 'Level', required: true
},
issued_date: {
type: Date, required: true
},
status: {
type: String, enum: ['active', 'revoked'], default: 'active'
},
// Optional personal info
passport_number: { type: String, trim: true },
address: { type: String, trim: true },
// Document image
certificate_image: { type: String }
}, { timestamps: true });
module.exports = mongoose.model('Certificate', certificateSchema);

View File

@@ -1,423 +0,0 @@
const mongoose = require("mongoose");
// Schema cho hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
backgroundImage: {
type: String,
trim: true,
default: "",
},
overlayColor: {
type: String,
trim: true,
default: "rgba(0, 0, 0, 0)",
},
sectionClass: {
type: String,
trim: true,
default: "",
},
titleClass: {
type: String,
trim: true,
default: "",
},
enableScrollspy: {
type: Boolean,
default: false,
},
backgroundPosition: {
type: String,
trim: true,
default: "center",
},
},
{ _id: false }
);
// Schema cho contact card
const contactCardSchema = new mongoose.Schema(
{
type: {
type: String,
required: true,
trim: true,
enum: [
"phone",
"email",
"location",
"hours",
"website",
"social",
"custom",
],
},
title: {
type: String,
required: true,
trim: true,
},
content: {
type: [String],
default: [],
},
iconType: {
type: String,
required: false,
trim: true,
default: "",
},
iconSource: {
type: String,
required: false,
trim: true,
enum: ["fontawesome", "image"],
default: "fontawesome",
},
},
{ _id: false }
);
// Schema cho map coordinates
const coordinatesSchema = new mongoose.Schema(
{
lat: {
type: Number,
required: true,
},
lng: {
type: Number,
required: true,
},
},
{ _id: false }
);
// Schema cho tile layer
const tileLayerSchema = new mongoose.Schema(
{
url: {
type: String,
required: true,
trim: true,
},
attribution: {
type: String,
trim: true,
default: "",
},
maxZoom: {
type: Number,
default: 18,
},
minZoom: {
type: Number,
default: 0,
},
},
{ _id: false }
);
// Schema cho map
const mapSchema = new mongoose.Schema(
{
coordinates: {
type: coordinatesSchema,
required: true,
},
zoom: {
type: Number,
default: 15,
},
location: {
type: String,
required: true,
trim: true,
},
markerTitle: {
type: String,
trim: true,
default: "",
},
embedUrl: {
type: String,
trim: true,
default: "",
},
tileLayer: {
type: tileLayerSchema,
required: true,
},
},
{ _id: false }
);
// Schema cho form field
const formFieldSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
label: {
type: String,
trim: true,
default: "",
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "programme", "date"],
},
placeholder: {
type: String,
trim: true,
default: "",
},
required: {
type: Boolean,
default: false,
},
colClass: {
type: String,
trim: true,
default: "col-lg-12",
},
programmeName: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema cho submit button
const submitButtonSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
},
icon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
buttonClass: {
type: String,
trim: true,
default: "theme-btn style-2",
},
},
{ _id: false }
);
// Schema cho form
const formSchema = new mongoose.Schema(
{
sectionLabel: {
type: String,
trim: true,
default: "",
},
heading: {
type: String,
trim: true,
default: "",
},
description: {
type: String,
trim: true,
default: "",
},
fields: {
type: [formFieldSchema],
default: [],
},
submitButton: {
type: submitButtonSchema,
required: true,
},
},
{ _id: false }
);
// Main Contact Schema
const contactSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
required: true,
},
contactCards: {
type: [contactCardSchema],
default: [],
},
map: {
type: mapSchema,
required: true,
},
form: {
type: formSchema,
required: true,
},
},
{
timestamps: true,
}
);
// Mapping iconType cũ sang Font Awesome icon mới
const iconTypeMapping = {
phone: "fas fa-phone",
email: "fas fa-envelope",
location: "fas fa-map-marker-alt",
clock: "fas fa-clock",
hours: "fas fa-clock",
};
// Tạo migration script để import dữ liệu từ contact-data.json
contactSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Kiểm tra xem đã có contact mặc định chưa
const existingContact = await this.findOne({ name: "default" });
// Xử lý và chuẩn hóa dữ liệu từ JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Contact Us",
backgroundImage: jsonData.hero?.backgroundImage || "",
overlayColor: jsonData.hero?.overlayColor || "rgba(0, 0, 0, 0)",
sectionClass: jsonData.hero?.sectionClass || "",
titleClass: jsonData.hero?.titleClass || "",
enableScrollspy: jsonData.hero?.enableScrollspy || false,
backgroundPosition: jsonData.hero?.backgroundPosition || "center",
},
contactCards: (jsonData.contactCards || []).map((card) => {
let iconType = card.iconType || "";
let iconSource = card.iconSource;
// Nếu không có iconSource, tự động detect từ iconType
if (!iconSource) {
// Nếu iconType là image path (bắt đầu bằng /uploads/ hoặc http)
if (
iconType.startsWith("/uploads/") ||
iconType.startsWith("http://") ||
iconType.startsWith("https://")
) {
iconSource = "image";
} else {
// Nếu iconType là string cũ (phone, email, location, clock)
iconSource = "fontawesome";
// Map iconType cũ sang Font Awesome icon mới
if (iconTypeMapping[iconType]) {
iconType = iconTypeMapping[iconType];
} else if (
iconType &&
!iconType.startsWith("fas ") &&
!iconType.startsWith("fab ")
) {
// Nếu iconType không phải là Font Awesome class hợp lệ, thử map
iconType = iconTypeMapping[iconType] || iconType;
}
}
} else {
// Nếu đã có iconSource nhưng iconType là string cũ, map sang Font Awesome
if (
iconSource === "fontawesome" &&
iconType &&
!iconType.startsWith("fas ") &&
!iconType.startsWith("fab ") &&
iconTypeMapping[iconType]
) {
iconType = iconTypeMapping[iconType];
}
}
return {
type: card.type || "custom",
title: card.title || "",
content: Array.isArray(card.content) ? card.content : [],
iconType: iconType,
iconSource: iconSource || "fontawesome",
};
}),
map: {
coordinates: {
lat: jsonData.map?.coordinates?.lat || 0,
lng: jsonData.map?.coordinates?.lng || 0,
},
zoom: jsonData.map?.zoom || 15,
location: jsonData.map?.location || "",
markerTitle: jsonData.map?.markerTitle || "",
embedUrl: jsonData.map?.embedUrl || "",
tileLayer: {
url:
jsonData.map?.tileLayer?.url ||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: jsonData.map?.tileLayer?.attribution || "",
maxZoom: jsonData.map?.tileLayer?.maxZoom || 18,
minZoom: jsonData.map?.tileLayer?.minZoom || 0,
},
},
form: {
sectionLabel: jsonData.form?.sectionLabel || "",
heading: jsonData.form?.heading || "",
description: jsonData.form?.description || "",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
label: field.label || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
colClass: field.colClass || "col-lg-12",
programmeName: field.programmeName || "",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Send Message",
icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right",
buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn style-2",
},
},
};
if (existingContact) {
// Cập nhật contact hiện có với dữ liệu đã xử lý
existingContact.hero = processedData.hero;
existingContact.contactCards = processedData.contactCards;
existingContact.map = processedData.map;
existingContact.form = processedData.form;
await existingContact.save();
console.log("Contact data updated successfully");
return existingContact;
} else {
// Tạo contact mới với dữ liệu đã xử lý
const newContact = await this.create({
name: "default",
...processedData,
});
console.log("Contact data imported successfully");
return newContact;
}
} catch (error) {
console.error("Error migrating contact data:", error);
throw error;
}
};
module.exports = mongoose.model("Contact", contactSchema);

View File

@@ -1,74 +0,0 @@
const mongoose = require("mongoose");
/**
* Schema for Contact Form Submissions
* Stores user inquiries from the contact form
*/
const contactSubmissionSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, "Name is required"],
trim: true,
maxlength: [100, "Name cannot exceed 100 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"],
},
phone: {
type: String,
trim: true,
default: "",
},
address: {
type: String,
trim: true,
default: "",
},
date: {
type: String,
trim: true,
default: "",
},
message: {
type: String,
trim: true,
default: "",
},
status: {
type: String,
enum: ["pending", "read", "replied", "archived"],
default: "pending",
},
notes: {
type: String,
trim: true,
default: "",
},
repliedAt: {
type: Date,
default: null,
},
ipAddress: {
type: String,
default: "",
},
userAgent: {
type: String,
default: "",
},
},
{
timestamps: true,
}
);
// Index for faster queries
contactSubmissionSchema.index({ status: 1, createdAt: -1 });
contactSubmissionSchema.index({ email: 1 });
module.exports = mongoose.model("ContactSubmission", contactSubmissionSchema);

88
models/degree.js Normal file
View File

@@ -0,0 +1,88 @@
const mongoose = require('mongoose');
const degreeSchema = new mongoose.Schema({
// Required fields
qualification_number: {
type: String,
required: true,
unique: true,
trim: true
},
student_name: {
type: String,
required: true,
trim: true
},
program_name: {
type: String,
required: true,
trim: true
},
type: {
type: String,
required: true,
enum: ['qualification', 'certification']
},
department: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Department',
required: true
},
level: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Level',
required: true
},
issued_date: {
type: Date,
required: true
},
status: {
type: String,
enum: ['active', 'revoked'],
default: 'active'
},
// Optional fields
certification_number: {
type: String,
trim: true,
},
passport_number: {
type: String,
trim: true
},
address: {
type: String,
trim: true
},
topic_name: {
type: String,
trim: true
},
topic_short_desc: {
type: String,
trim: true
},
degree_image: {
type: String
},
certificate_image: {
type: String
}
}, {
timestamps: true
});
// Indexes
degreeSchema.index({ certification_number: 1 }, { unique: true, sparse: true });
// Pre-save hook: certification type requires certification_number
degreeSchema.pre('save', function (next) {
if (this.type === 'certification' && !this.certification_number) {
return next(new Error('certification_number is required for certification type'));
}
next();
});
module.exports = mongoose.model('Degree', degreeSchema);

View File

@@ -1,222 +0,0 @@
const mongoose = require('mongoose');
const faqItemSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
description: {
type: String,
required: true
}
}, { _id: true });
const faqSectionSchema = new mongoose.Schema({
id: {
type: String,
required: true
},
title: {
type: String,
required: true
},
faqs: [faqItemSchema]
}, { _id: true });
const sidebarNavSchema = new mongoose.Schema({
id: {
type: String,
required: true
},
label: {
type: String,
required: true
}
});
const heroSchema = new mongoose.Schema({
title: String,
backgroundImage: String,
overlayColor: String,
sectionClass: String,
titleClass: String,
enableScrollspy: Boolean,
backgroundPosition: String
});
const contactBoxSchema = new mongoose.Schema({
title: String,
phone: {
icon: String,
text: String
},
email: {
icon: String,
text: String
}
});
const videoSchema = new mongoose.Schema({
url: String,
title: String
});
const faqSchema = new mongoose.Schema({
name: {
type: String,
default: 'default',
},
hero: heroSchema,
sidebarNav: [sidebarNavSchema],
contactBox: contactBoxSchema,
faqSections: [faqSectionSchema],
video: videoSchema
}, {
timestamps: true
});
// Virtual property để tính tổng số FAQ items
faqSchema.virtual('totalFaqs').get(function() {
return this.faqSections.reduce((total, section) => {
return total + (section.faqs ? section.faqs.length : 0);
}, 0);
});
// Static method: Lấy FAQ mặc định
faqSchema.statics.getDefault = async function() {
let faq = await this.findOne({ name: 'default' });
// Nếu không có, tạo mới
if (!faq) {
faq = new this({
name: 'default',
hero: {
title: 'Frequently Asked Questions',
backgroundImage: 'yootheme/cache/18/faqs_header_new.jpg',
overlayColor: 'rgba(0, 0, 0, 0)',
sectionClass: 'uk-section-secondary uk-section-overlap uk-preserve-color uk-light',
titleClass: 'uk-heading-large uk-text-center !text-[5vw]',
enableScrollspy: true,
backgroundPosition: 'top-center'
},
sidebarNav: [
{ id: 'general-information', label: 'General Information' }
],
contactBox: {
title: 'Let\'s plan your perfect nature escape',
phone: { icon: 'phone', text: '+(123)-456-789' },
email: { icon: 'email', text: 'hello@ggcamp.org' }
},
faqSections: [
{
id: 'general-information',
title: 'General Information',
faqs: [
{
title: 'Sample FAQ Question',
description: 'This is a sample FAQ answer. Please update with your actual content.'
}
]
}
]
});
await faq.save();
}
return faq;
};
// Static method: Import từ JSON
faqSchema.statics.importFromJson = async function(data) {
let faq = await this.findOne({ name: 'default' });
// Đảm bảo có name
const faqData = {
name: 'default',
...data
};
if (!faq) {
faq = new this(faqData);
} else {
// Update các trường
Object.keys(faqData).forEach(key => {
faq[key] = faqData[key];
});
}
await faq.save();
return faq;
};
// Method: Thêm FAQ vào section
faqSchema.methods.addFaqToSection = async function(sectionId, faqItem) {
const section = this.faqSections.find(s => s.id === sectionId);
if (!section) {
throw new Error(`Section with id ${sectionId} not found`);
}
section.faqs.push(faqItem);
await this.save();
return this;
};
// Method: Update FAQ item
faqSchema.methods.updateFaqItem = async function(sectionId, faqId, updates) {
const section = this.faqSections.find(s => s.id === sectionId);
if (!section) {
throw new Error(`Section with id ${sectionId} not found`);
}
const faqItem = section.faqs.id(faqId);
if (!faqItem) {
throw new Error(`FAQ item with id ${faqId} not found in section ${sectionId}`);
}
if (updates.title !== undefined) faqItem.title = updates.title;
if (updates.description !== undefined) faqItem.description = updates.description;
await this.save();
return this;
};
// Method: Delete FAQ item
faqSchema.methods.deleteFaqItem = async function(sectionId, faqId) {
const section = this.faqSections.find(s => s.id === sectionId);
if (!section) {
throw new Error(`Section with id ${sectionId} not found`);
}
const faqItem = section.faqs.id(faqId);
if (!faqItem) {
throw new Error(`FAQ item with id ${faqId} not found in section ${sectionId}`);
}
section.faqs.pull(faqId);
await this.save();
return this;
};
// Method: Update FAQ section
faqSchema.methods.updateFaqSection = async function(sectionId, updates) {
const section = this.faqSections.find(s => s.id === sectionId);
if (!section) {
throw new Error(`Section with id ${sectionId} not found`);
}
if (updates.title !== undefined) section.title = updates.title;
await this.save();
return this;
};
const FAQ = mongoose.model('FAQ', faqSchema);
module.exports = FAQ;

View File

@@ -1,213 +0,0 @@
const mongoose = require("mongoose");
// Schema cho menu links
const menuLinkSchema = new mongoose.Schema(
{
label: {
type: String,
required: true,
trim: true,
},
href: {
type: String,
required: true,
trim: true,
},
order: {
type: Number,
required: false,
default: 0,
},
},
{ _id: false },
);
// Schema cho social links
const socialLinkSchema = new mongoose.Schema(
{
icon: {
type: String,
required: true,
trim: true,
},
href: {
type: String,
required: true,
trim: true,
},
},
{ _id: false },
);
// Schema cho phone
const phoneSchema = new mongoose.Schema(
{
display: {
type: String,
required: false,
trim: true,
default: "",
},
href: {
type: String,
required: false,
trim: true,
default: "",
},
},
{ _id: false },
);
// Schema cho logo
const logoSchema = new mongoose.Schema(
{
src: {
type: String,
required: false,
trim: true,
default: "",
},
alt: {
type: String,
required: false,
trim: true,
default: "",
},
href: {
type: String,
required: false,
trim: true,
default: "/",
},
},
{ _id: false },
);
// Schema cho copyright
const copyrightSchema = new mongoose.Schema(
{
text: {
type: String,
required: false,
trim: true,
default: "Copyright©",
},
brand: {
type: String,
required: false,
trim: true,
default: "",
},
rights: {
type: String,
required: false,
trim: true,
default: "All Rights Reserved.",
},
},
{ _id: false },
);
// Schema cho top section
const topSchema = new mongoose.Schema(
{
bgImage: {
type: String,
required: false,
trim: true,
default: "",
},
phone: {
type: phoneSchema,
default: () => ({ display: "", href: "" }),
},
address: {
type: String,
required: false,
trim: true,
default: "",
},
logo: {
type: logoSchema,
default: () => ({ src: "", alt: "", href: "/" }),
},
menuLinks: {
type: [menuLinkSchema],
default: [],
},
socialLinks: {
type: [socialLinkSchema],
default: [],
},
},
{ _id: false },
);
// Schema cho bottom section
const bottomSchema = new mongoose.Schema(
{
copyright: {
type: copyrightSchema,
default: () => ({ text: "Copyright©", brand: "", rights: "All Rights Reserved." }),
},
menuLinks: {
type: [menuLinkSchema],
default: [],
},
},
{ _id: false },
);
// Main Footer Schema - khớp 100% với footer.json
const footerSchema = new mongoose.Schema(
{
top: {
type: topSchema,
default: () => ({
bgImage: "",
phone: { display: "", href: "" },
address: "",
logo: { src: "", alt: "", href: "/" },
menuLinks: [],
socialLinks: [],
}),
},
bottom: {
type: bottomSchema,
default: () => ({
copyright: { text: "Copyright©", brand: "", rights: "All Rights Reserved." },
menuLinks: [],
}),
},
},
{
timestamps: true,
},
);
// Static method để lấy hoặc tạo footer duy nhất
footerSchema.statics.getSingle = async function () {
let footer = await this.findOne();
if (!footer) {
footer = await this.create({});
}
return footer;
};
// Migration method để import từ JSON hiện tại
footerSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Xóa tất cả documents hiện có
await this.deleteMany({});
// Tạo document mới
const footer = await this.create(jsonData);
console.log("Footer data migrated successfully");
return footer;
} catch (error) {
console.error("Error migrating footer data:", error);
throw error;
}
};
module.exports = mongoose.model("Footer", footerSchema);

View File

@@ -1,51 +0,0 @@
const mongoose = require('mongoose');
const formSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
unique: true
},
admission: {
background_image: String,
title: String,
year: String,
description: String,
form: {
fields: [{
type: { type: String },
placeholder: String
}],
button: {
text: String,
url: String
}
}
},
apply: {
title: String,
steps: [{
title: String,
description: String
}]
},
application_form: {
title: String,
question: String,
button: {
text: String,
icon: String,
url: String
},
links: [{
text: String,
url: String
}]
},
updatedAt: Date
}, {
timestamps: true
});
module.exports = mongoose.model('Form', formSchema);

View File

@@ -1,115 +0,0 @@
const mongoose = require("mongoose");
const socialLinkSchema = new mongoose.Schema(
{
platform: {
type: String,
required: true,
enum: ["linkedin", "twitter", "instagram", "youtube", "facebook"],
},
url: {
type: String,
required: true,
},
icon: String,
order: {
type: Number,
default: 0,
},
},
{ _id: false },
);
const languageSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
},
{ _id: false },
);
const menuItemSchema = new mongoose.Schema(
{
label: {
type: String,
required: true,
},
href: {
type: String,
required: true,
},
icon: String,
order: {
type: Number,
default: 0,
},
children: [this],
},
{ _id: false },
);
const headerSchema = new mongoose.Schema(
{
// Top bar
top: {
phone: String,
email: String,
location: String,
socialLinks: [socialLinkSchema],
languages: [languageSchema],
},
// Offcanvas
offcanvas: {
description: String,
contactInfo: {
address: String,
email: String,
workingHours: String,
phone: String,
},
},
// Menu
menu: [menuItemSchema],
// Logo
logo: {
light: String,
dark: String,
alt: String,
},
// CTA Button
ctaButton: {
label: String,
href: String,
style: {
type: String,
enum: ["primary", "secondary", "outline"],
default: "primary",
},
},
// Status
status: {
type: String,
enum: ["active", "inactive"],
default: "active",
},
order: {
type: Number,
default: 1,
},
},
{ timestamps: true },
);
module.exports = mongoose.model("Header", headerSchema);

View File

@@ -1,48 +0,0 @@
const mongoose = require('mongoose');
const HeaderMenuSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
trim: true,
lowercase: true
},
url: {
type: String,
required: true,
trim: true
},
parentId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'HeaderMenu',
default: null
},
order: {
type: Number,
default: 0
},
status: {
type: String,
enum: ['active', 'inactive'],
default: 'active'
},
type: {
type: String,
enum: ['internal', 'external'],
default: 'internal'
}
}, {
timestamps: true
});
// Indexes for optimization
HeaderMenuSchema.index({ order: 1 });
HeaderMenuSchema.index({ status: 1 });
HeaderMenuSchema.index({ parentId: 1, order: 1 });
module.exports = mongoose.model('HeaderMenu', HeaderMenuSchema);

View File

@@ -1,96 +0,0 @@
const mongoose = require('mongoose');
const heroSchema = new mongoose.Schema({
hero: {
title: {
type: String,
required: true
},
description: {
type: String,
required: true
},
backgroundImage: {
type: String,
required: true
},
overlayColor: {
type: String,
default: 'rgba(0, 0, 0, 0.35)'
},
enableScrollspy: {
type: Boolean,
default: true
},
backgroundPosition: {
type: String,
default: 'center center'
},
containerStyles: {
width: { type: String, default: '98%' },
height: { type: String, default: '130vh' },
margin: { type: String, default: '0 auto' },
borderRadius: { type: String, default: '2vw' },
overflow: { type: String, default: 'hidden' },
position: { type: String, default: 'relative' },
top: { type: String, default: '-10vh' }
},
titleClass: {
type: String,
default: 'uk-heading-large uk-text-center uk-text-white'
},
titleStyles: {
fontSize: { type: String, default: 'clamp(2rem, 5vw, 4.5rem)' },
fontWeight: { type: String, default: 'bold' },
lineHeight: { type: String, default: '1.2' },
marginBottom: { type: String, default: '1.5rem' },
color: { type: String, default: 'white' },
textShadow: { type: String, default: '0 2px 10px rgba(0, 0, 0, 0.3)' }
},
descriptionClass: {
type: String,
default: 'uk-text-white'
},
descriptionStyles: {
fontSize: { type: String, default: 'clamp(1rem, 1.5vw, 1.25rem)' },
maxWidth: { type: String, default: '800px' },
margin: { type: String, default: '0 auto 2rem' },
lineHeight: { type: String, default: '1.6' },
color: { type: String, default: 'white' },
textShadow: { type: String, default: '0 1px 5px rgba(0, 0, 0, 0.3)' }
}
},
button: {
label: {
type: String,
default: 'Book Your Adventure'
},
href: {
type: String,
default: '/booking'
},
type: {
type: String,
default: 'magic'
}
},
contactBox: {
enabled: {
type: Boolean,
default: true
},
position: {
position: { type: String, default: 'absolute' },
bottom: { type: String, default: '3rem' },
left: { type: String, default: '50%' },
transform: { type: String, default: 'translateX(-50%)' },
width: { type: String, default: '100%' },
zIndex: { type: Number, default: 3 },
padding: { type: String, default: '0 1rem' }
}
}
}, {
timestamps: true
});
module.exports = mongoose.model('Hero', heroSchema);

View File

@@ -1,277 +0,0 @@
const mongoose = require("mongoose");
const { Schema } = mongoose;
// Reusable small schemas
const LinkSchema = new Schema(
{
label: { type: String, default: "" },
href: { type: String, default: "" },
},
{ _id: false },
);
// Hero slide (for multiple hero items in slider)
const HeroSlideSchema = new Schema(
{
title: { type: String, default: "" },
subtitle: { type: String, default: "" },
description: { type: String, default: "" },
primaryButton: { type: LinkSchema, default: () => ({}) },
secondaryButton: { type: LinkSchema, default: () => ({}) },
heroImage: { type: String, default: "" },
videoUrl: { type: String, default: "" },
},
{ _id: false },
);
const HeroSchema = new Schema(
{
// Background for whole hero section
backgroundImage: { type: String, default: "" },
// Multiple slides
slides: { type: [HeroSlideSchema], default: [] },
// Legacy single-slide fields (backward compatible)
title: { type: String, default: "" },
subtitle: { type: String, default: "" },
description: { type: String, default: "" },
primaryButton: { type: LinkSchema, default: () => ({}) },
secondaryButton: { type: LinkSchema, default: () => ({}) },
heroImage: { type: String, default: "" },
videoUrl: { type: String, default: "" },
},
{ _id: false },
);
const WhyChooseUsItemSchema = new Schema(
{
icon: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
},
{ _id: false },
);
const WhyChooseUsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
description: { type: String, default: "" },
highlightWord: { type: String, default: "" },
mainImage: { type: String, default: "" },
secondaryImage: { type: String, default: "" },
items: { type: [WhyChooseUsItemSchema], default: [] },
features: { type: [String], default: [] },
ctaButton: { type: LinkSchema, default: () => ({}) },
},
{ _id: false },
);
const VisaSolutionItemSchema = new Schema(
{
number: { type: String, default: "" },
title: { type: String, default: "" },
description: { type: String, default: "" },
link: { type: String, default: "" },
},
{ _id: false },
);
const VisaSolutionsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
items: { type: [VisaSolutionItemSchema], default: [] },
},
{ _id: false },
);
const VisaCountrySchema = new Schema(
{
name: { type: String, default: "" },
code: { type: String, default: "" },
flag: { type: String, default: "" },
link: { type: String, default: "" },
visaTypes: { type: [String], default: [] },
},
{ _id: false },
);
const VisaCountriesSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
description: { type: String, default: "" },
countries: { type: [VisaCountrySchema], default: [] },
ctaButton: { type: LinkSchema, default: () => ({}) },
},
{ _id: false },
);
const TestimonialSchema = new Schema(
{
name: { type: String, default: "" },
role: { type: String, default: "" },
country: { type: String, default: "" },
rating: { type: Number, default: 5 },
comment: { type: String, default: "" },
avatar: { type: String, default: "" },
},
{ _id: false },
);
const TestimonialsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
videoUrl: { type: String, default: "" },
videoThumbnail: { type: String, default: "" },
items: { type: [TestimonialSchema], default: [] },
},
{ _id: false },
);
const VideoGallerySchema = new Schema(
{
heading: { type: String, default: "" },
videoUrl: { type: String, default: "" },
thumbnail: { type: String, default: "" },
},
{ _id: false },
);
const FaqItemSchema = new Schema(
{
question: { type: String, default: "" },
answer: { type: String, default: "" },
},
{ _id: false },
);
const FaqSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
description: { type: String, default: "" },
ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [FaqItemSchema], default: [] },
},
{ _id: false },
);
const AchievementItemSchema = new Schema(
{
value: { type: String, default: "" },
suffix: { type: String, default: "" },
label: { type: String, default: "" },
description: { type: String, default: "" },
},
{ _id: false },
);
const AchievementsSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
items: { type: [AchievementItemSchema], default: [] },
},
{ _id: false },
);
const VisaConsultancyItemSchema = new Schema(
{
name: { type: String, default: "" },
icon: { type: String, default: "" },
year: { type: String, default: "" },
},
{ _id: false },
);
const VisaConsultancySchema = new Schema(
{
items: { type: [VisaConsultancyItemSchema], default: [] },
},
{ _id: false },
);
const BrandItemSchema = new Schema(
{
logo: { type: String, default: "" },
},
{ _id: false },
);
const BrandsSchema = new Schema(
{
items: { type: [BrandItemSchema], default: [] },
},
{ _id: false },
);
const PartnersSchema = new Schema(
{
visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) },
brands: { type: BrandsSchema, default: () => ({}) },
},
{ _id: false },
);
const BlogPreviewItemSchema = new Schema(
{
title: { type: String, default: "" },
excerpt: { type: String, default: "" },
category: { type: String, default: "" },
date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20")
author: {
name: { type: String, default: "" },
avatar: { type: String, default: "" },
},
comments: { type: Number, default: 0 },
link: { type: String, default: "" },
thumbnail: { type: String, default: "" },
},
{ _id: false },
);
const BlogPreviewSchema = new Schema(
{
heading: { type: String, default: "" },
subheading: { type: String, default: "" },
ctaButton: { type: LinkSchema, default: () => ({}) },
items: { type: [BlogPreviewItemSchema], default: [] },
selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }],
},
{ _id: false },
);
/**
* Home page content model
*
* NOTE:
* - This schema is based on `hailearning.edu.vn/app/home.json`.
* - `strict: false` keeps backward compatibility with any existing CMS-only sections
* (e.g. about/missionVision/programs/newsletter/latestPosts...) that the admin UI might still send.
*/
const HomeSchema = new Schema(
{
hero: { type: HeroSchema, default: () => ({}) },
whyChooseUs: { type: WhyChooseUsSchema, default: () => ({}) },
visaSolutions: { type: VisaSolutionsSchema, default: () => ({}) },
visaCountries: { type: VisaCountriesSchema, default: () => ({}) },
testimonials: { type: TestimonialsSchema, default: () => ({}) },
videoGallery: { type: VideoGallerySchema, default: () => ({}) },
faq: { type: FaqSchema, default: () => ({}) },
achievements: { type: AchievementsSchema, default: () => ({}) },
partners: { type: PartnersSchema, default: () => ({}) },
blogPreview: { type: BlogPreviewSchema, default: () => ({}) },
},
{
timestamps: true,
strict: false,
},
);
module.exports = mongoose.model("Home", HomeSchema);

View File

@@ -1,302 +0,0 @@
const mongoose = require("mongoose");
// Schema cho content items
const contentItemSchema = new mongoose.Schema(
{
type: {
type: String,
enum: ["paragraph", "section", "list", "note", "embed", "header"],
required: true,
},
text: {
type: String,
trim: true,
default: "",
},
title: {
type: String,
trim: true,
default: "",
},
content: {
type: String,
trim: true,
default: "",
},
items: {
type: [String],
default: [],
},
level: {
type: Number,
default: 2,
},
// Embed/video fields
embed: {
type: String,
trim: true,
default: ''
},
url: {
type: String,
trim: true,
default: ''
},
source: {
type: String,
trim: true,
default: ''
},
videoId: {
type: String,
trim: true,
default: ''
},
caption: {
type: String,
trim: true,
default: ''
},
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
},
{ _id: false }
);
// Schema cho overlay style
const overlayStyleSchema = new mongoose.Schema(
{
backgroundColor: {
type: String,
trim: true,
default: "rgba(0, 0, 0, 0)",
},
},
{ _id: false }
);
// Schema cho hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
default: "Insurance & Travel Cancellation Guarantee",
},
subtitle: {
type: String,
trim: true,
default: "Comprehensive coverage for your peace of mind",
},
backgroundImage: {
type: String,
trim: true,
default: "/uploads/banner/b13.jpg",
},
sectionClass: {
type: String,
trim: true,
default: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
},
backgroundClasses: {
type: String,
trim: true,
default: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
},
overlayStyle: {
type: overlayStyleSchema,
default: () => ({ backgroundColor: "rgba(0, 0, 0, 0)" }),
},
titleClass: {
type: String,
trim: true,
default: "text-white text-[5vw] uk-text-center",
},
subtitleClass: {
type: String,
trim: true,
default: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
},
enableScrollspy: {
type: Boolean,
default: true,
},
},
{ _id: false }
);
// Schema cho page section
const pageSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
default: "Insurance & Travel Information",
},
divider: {
type: Boolean,
default: true,
},
sectionClass: {
type: String,
trim: true,
default: "uk-section-default uk-section-overlap uk-section",
},
titleClass: {
type: String,
trim: true,
default: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
},
dividerClass: {
type: String,
trim: true,
default: "uk-divider-small uk-text-left@m uk-text-center",
},
},
{ _id: false }
);
// Schema cho content section
const contentSchema = new mongoose.Schema(
{
sectionClass: {
type: String,
trim: true,
default: "uk-section-muted uk-section-overlap uk-section",
},
textClass: {
type: String,
trim: true,
default: "uk-panel uk-margin text-[1vw]",
},
content: {
type: [contentItemSchema],
default: [],
},
},
{ _id: false }
);
// Main Insurance Schema - CẤU TRÚC MỚI
const insuranceSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
// 3 PHẦN CHÍNH
hero: {
type: heroSchema,
required: true,
},
page: {
type: pageSchema,
required: true,
},
content: {
type: contentSchema,
required: true,
},
language: {
type: String,
default: "en",
},
version: {
type: String,
default: "2.0.0",
},
isActive: {
type: Boolean,
default: true,
},
migratedFromOldStructure: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);
// Static method: Lấy insurance default
insuranceSchema.statics.getDefault = async function(language = "en") {
try {
let insurance = await this.findOne({ name: "default", language: language });
if (!insurance) {
// Tạo default data nếu chưa có
insurance = await this.create({
name: "default",
language: language,
hero: {
title: "Insurance & Travel Cancellation Guarantee",
subtitle: "Comprehensive coverage for your peace of mind",
backgroundImage: "/uploads/banner/b13.jpg",
},
page: {
title: "Insurance & Travel Information",
divider: true,
},
content: {
content: []
}
});
}
return insurance;
} catch (error) {
console.error("Error in getDefault:", error);
throw error;
}
};
// Method để get insurance data
insuranceSchema.methods.getInsuranceData = function() {
return this.toObject();
};
// Migration method - chỉ hỗ trợ cấu trúc mới
insuranceSchema.statics.migrateFromJson = async function(jsonData, language = "en") {
try {
console.log('Migrating insurance from JSON...');
// Xóa document cũ nếu có
await this.deleteOne({ name: "default", language: language });
// Sử dụng dữ liệu từ JSON trực tiếp
const processedData = {
name: "default",
language: language,
version: "2.0.0",
isActive: true,
hero: jsonData.hero,
page: jsonData.page,
content: jsonData.content
};
// Tạo document mới
const newInsurance = await this.create(processedData);
const contentItems = jsonData.content?.content || [];
console.log(`Insurance data migrated successfully for language: ${language}`);
console.log(`Total content items: ${contentItems.length}`);
return newInsurance;
} catch (error) {
console.error("Error migrating insurance data to new structure:", error);
throw error;
}
};
const Insurance = mongoose.model("Insurance", insuranceSchema);
module.exports = Insurance;

View File

@@ -1,67 +1,14 @@
const mongoose = require('mongoose');
const levelSchema = new mongoose.Schema({
brochure: { type: String },
type: {
type: String,
required: true,
unique: true,
trim: true
},
banner: {
image: String,
title: String,
text: String
},
overview: {
title: String,
paragraphs: [String],
contact_info: {
title: String,
subtitle: String,
items: [{
text: String
}]
},
social_info: {
title: String,
social_links: [{
image: String,
url: String,
alt: String
}],
apply_button: {
text: String,
url: String
}
}
},
requirements: {
title: String,
items: [String]
},
action_buttons: {
title: String,
buttons: [{
text: String,
link: String
}]
},
why_study: {
title: String,
items: [{
number: String,
title: String,
text: String
}]
},
// Thêm tham chiếu đến Form
form: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Form'
},
updatedAt: Date
}
}, {
timestamps: true
});
module.exports = mongoose.model('Level', levelSchema);
module.exports = mongoose.model('Level', levelSchema);

View File

@@ -1,185 +0,0 @@
const mongoose = require('mongoose');
const MenuSchema = new mongoose.Schema({
menuid: { type: String, required: true, unique: true }, // ID tùy chỉnh
parent: { type: String, default: null }, // ID menu cha
title: { type: String, required: true }, // Tên hiển thị
url: { type: String, default: '' }, // Đường dẫn
order: { type: Number, default: 0 }, // Thứ tự hiển thị
type: { type: String, enum: ['static', 'page', 'level'], default: 'static' }, // Loại menu
fetch: { type: Boolean, default: false }, // Có fetch programme không
isActive: { type: Boolean, default: true } // Trạng thái hoạt động của menu
}, { timestamps: false });
// Index để tối ưu query
MenuSchema.index({ parent: 1, order: 1 });
MenuSchema.index({ type: 1 });
// Method để lấy menu tree
MenuSchema.statics.getMenuTree = async function () {
try {
const allMenus = await this.find().sort({ order: 1 }).lean();
const menuMap = new Map();
const rootMenus = [];
// Đưa tất cả menus vào map và xử lý URL dựa trên isActive
allMenus.forEach(menu => {
const activeUrl = menu.isActive === false ? '/maintenance/' : menu.url;
menuMap.set(menu.menuid, {
...menu,
url: activeUrl, // Sử dụng URL đã được xử lý
children: []
});
});
// Xây dựng tree structure
allMenus.forEach(menu => {
const menuObj = menuMap.get(menu.menuid);
if (!menu.parent) {
rootMenus.push(menuObj);
} else {
const parent = menuMap.get(menu.parent);
if (parent) {
parent.children.push(menuObj);
}
}
});
return rootMenus;
} catch (error) {
console.error('Error building menu tree:', error);
throw error;
}
};
// Method để lấy menu tree với programmes
MenuSchema.statics.getMenuTreeWithProgrammes = async function () {
try {
const menuTree = await this.getMenuTree();
// Thêm programmes cho các menu level
for (const menu of menuTree) {
await this.addProgrammesToMenu(menu);
}
return menuTree;
} catch (error) {
console.error('Error building menu tree with programmes:', error);
throw error;
}
};
// Method để thêm programmes vào menu
MenuSchema.statics.addProgrammesToMenu = async function (menuItem) {
try {
if (menuItem.type === 'level' && menuItem.fetch) {
const programmes = await this.getProgrammesByMenuId(menuItem.menuid);
menuItem.programmes = programmes;
}
if (menuItem.children && menuItem.children.length > 0) {
for (const child of menuItem.children) {
await this.addProgrammesToMenu(child);
}
}
} catch (error) {
console.error('Error adding programmes to menu:', error);
throw error;
}
};
// Method để lấy programmes theo menu ID
MenuSchema.statics.getProgrammesByMenuId = async function (menuId) {
try {
const Programme = require('./programme');
const Level = require('./level');
// Sử dụng trực tiếp menuId làm levelType vì menuid đã được đặt đúng khi tạo
const levelType = menuId;
const level = await Level.findOne({ type: levelType });
if (!level) return [];
const programmes = await Programme.find({ level: level._id })
.select('name code level_type')
.sort({ name: 1 })
.lean();
return programmes;
} catch (error) {
console.error('Error getting programmes by menu ID:', error);
throw error;
}
};
// Method để tạo menu mới
MenuSchema.statics.createMenu = async function (menuData) {
try {
const menu = new this(menuData);
await menu.save();
return menu;
} catch (error) {
console.error('Error creating menu:', error);
throw error;
}
};
// Method để cập nhật menu
MenuSchema.statics.updateMenu = async function (menuId, updateData) {
try {
const menu = await this.findOneAndUpdate({ menuid: menuId }, updateData, { new: true });
return menu;
} catch (error) {
console.error('Error updating menu:', error);
throw error;
}
};
// Method để xóa menu
MenuSchema.statics.deleteMenu = async function (menuId) {
try {
// Xóa tất cả children trước
await this.deleteMany({ parent: menuId });
// Sau đó xóa menu chính
const result = await this.findOneAndDelete({ menuid: menuId });
return result;
} catch (error) {
console.error('Error deleting menu:', error);
throw error;
}
};
// Method để sắp xếp lại order
MenuSchema.statics.reorderMenus = async function (parentId, menuIds) {
try {
const updates = menuIds.map((menuId, index) => ({
updateOne: {
filter: { menuid: menuId },
update: { order: index }
}
}));
await this.bulkWrite(updates);
console.log('Menus reordered successfully');
} catch (error) {
console.error('Error reordering menus:', error);
throw error;
}
};
// Method để lấy URL dựa trên trạng thái isActive
MenuSchema.methods.getActiveUrl = function () {
if (this.isActive === false) {
return '/maintenance/';
}
return this.url;
};
// Method để kiểm tra trạng thái hoạt động
MenuSchema.methods.isMenuActive = function () {
return this.isActive !== false;
};
module.exports = mongoose.model('MenuHeader', MenuSchema);

View File

@@ -1,328 +0,0 @@
const mongoose = require("mongoose");
// Clear cache
if (mongoose.models.Pricing) {
delete mongoose.models.Pricing;
}
if (mongoose.connection.models.Pricing) {
delete mongoose.connection.models.Pricing;
}
// Schema for breadcrumb item
const breadcrumbItemSchema = new mongoose.Schema(
{
text: {
type: String,
trim: true,
default: "",
},
link: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for hero section
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
trim: true,
default: "Pricing Plan",
},
backgroundImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/breadcrumb.jpg",
},
shapeImage: {
type: String,
trim: true,
default: "/assets/img/inner-page/shape.png",
},
breadcrumb: {
type: [breadcrumbItemSchema],
default: [],
},
},
{ _id: false }
);
// Schema for pricing section header
const pricingSectionSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "pricing plan",
},
heading: {
type: String,
trim: true,
default: "Flexible Plans to Suit Every Traveler",
},
description: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for individual plan
const planSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: true,
},
price: {
type: String,
trim: true,
default: "0",
},
period: {
type: String,
trim: true,
default: "mo",
},
currency: {
type: String,
trim: true,
default: "$",
},
buttonText: {
type: String,
trim: true,
default: "Get Started Today",
},
buttonLink: {
type: String,
trim: true,
default: "/pricing",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
style: {
type: String,
trim: true,
enum: ["default", "style-2"],
default: "default",
},
features: {
type: [String],
default: [],
},
},
{ _id: false }
);
// Schema for plans container
const plansSchema = new mongoose.Schema(
{
monthly: {
type: [planSchema],
default: [],
},
yearly: {
type: [planSchema],
default: [],
},
},
{ _id: false }
);
// Schema for testimonial item
const testimonialItemSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
default: "",
},
role: {
type: String,
trim: true,
default: "",
},
rating: {
type: Number,
min: 1,
max: 5,
default: 5,
},
content: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema for testimonials section
const testimonialsSchema = new mongoose.Schema(
{
subtitle: {
type: String,
trim: true,
default: "What Our Clients Say",
},
heading: {
type: String,
trim: true,
default: "Immigration Success Stories",
},
buttonText: {
type: String,
trim: true,
default: "View All Review",
},
buttonLink: {
type: String,
trim: true,
default: "/contact",
},
buttonIcon: {
type: String,
trim: true,
default: "fa-solid fa-arrow-right",
},
image: {
type: String,
trim: true,
default: "",
},
items: {
type: [testimonialItemSchema],
default: [],
},
},
{ _id: false }
);
// Main Pricing Schema
const pricingSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
hero: {
type: heroSchema,
default: () => ({}),
},
pricingSection: {
type: pricingSectionSchema,
default: () => ({}),
},
plans: {
type: plansSchema,
default: () => ({}),
},
testimonials: {
type: testimonialsSchema,
default: () => ({}),
},
},
{
timestamps: true,
}
);
// Migration method to import data from JSON
pricingSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Check if default pricing exists
const existingPricing = await this.findOne({ name: "default" });
// Process data from JSON
const processedData = {
hero: {
title: jsonData.hero?.title || "Pricing Plan",
backgroundImage: jsonData.hero?.backgroundImage || "/assets/img/inner-page/breadcrumb.jpg",
shapeImage: jsonData.hero?.shapeImage || "/assets/img/inner-page/shape.png",
breadcrumb: (jsonData.hero?.breadcrumb || []).map((item) => ({
text: item.text || "",
link: item.link || "",
})),
},
pricingSection: {
subtitle: jsonData.pricingSection?.subtitle || "pricing plan",
heading: jsonData.pricingSection?.heading || "Flexible Plans to Suit Every Traveler",
description: jsonData.pricingSection?.description || "",
},
plans: {
monthly: (jsonData.plans?.monthly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
yearly: (jsonData.plans?.yearly || []).map((plan) => ({
name: plan.name || "",
price: plan.price || "0",
period: plan.period || "mo",
currency: plan.currency || "$",
buttonText: plan.buttonText || "Get Started Today",
buttonLink: plan.buttonLink || "/pricing",
buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right",
style: plan.style || "default",
features: plan.features || [],
})),
},
testimonials: {
subtitle: jsonData.testimonials?.subtitle || "What Our Clients Say",
heading: jsonData.testimonials?.heading || "Immigration Success Stories",
buttonText: jsonData.testimonials?.buttonText || "View All Review",
buttonLink: jsonData.testimonials?.buttonLink || "/contact",
buttonIcon: jsonData.testimonials?.buttonIcon || "fa-solid fa-arrow-right",
image: jsonData.testimonials?.image || "",
items: (jsonData.testimonials?.items || []).map((item) => ({
name: item.name || "",
role: item.role || "",
rating: item.rating || 5,
content: item.content || "",
})),
},
};
if (existingPricing) {
// Update existing pricing
existingPricing.hero = processedData.hero;
existingPricing.pricingSection = processedData.pricingSection;
existingPricing.plans = processedData.plans;
existingPricing.testimonials = processedData.testimonials;
await existingPricing.save();
console.log("Pricing data updated successfully");
return existingPricing;
} else {
// Create new pricing
const newPricing = await this.create({
name: "default",
...processedData,
});
console.log("Pricing data imported successfully");
return newPricing;
}
} catch (error) {
console.error("Error migrating pricing data:", error);
throw error;
}
};
module.exports = mongoose.model("Pricing", pricingSchema);

35
models/qualification.js Normal file
View File

@@ -0,0 +1,35 @@
const mongoose = require('mongoose');
const qualificationSchema = new mongoose.Schema({
qualification_number: {
type: String, required: true, unique: true, trim: true
},
student_name: {
type: String, required: true, trim: true
},
program_name: {
type: String, required: true, trim: true
},
department: {
type: mongoose.Schema.Types.ObjectId, ref: 'Department', required: true
},
level: {
type: mongoose.Schema.Types.ObjectId, ref: 'Level', required: true
},
issued_date: {
type: Date, required: true
},
status: {
type: String, enum: ['active', 'revoked'], default: 'active'
},
// Optional personal info
passport_number: { type: String, trim: true },
address: { type: String, trim: true },
// PhD fields — presence of topic_name signals PhD view on frontend
topic_name: { type: String, trim: true },
topic_short_desc: { type: String, trim: true },
// Document image
degree_image: { type: String }
}, { timestamps: true });
module.exports = mongoose.model('Qualification', qualificationSchema);

View File

@@ -1,79 +0,0 @@
const mongoose = require('mongoose');
// Recent Post model - có thể là view hoặc collection riêng để optimize performance
const recentPostSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
slug: {
type: String,
required: true,
trim: true
},
thumbnail: {
type: String,
default: '' // Ảnh nhỏ ở sidebar
},
publishedAt: {
type: String, // "March 26, 2025"
required: true
},
// Reference to original blog post
originalPostId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Blog',
required: true
}
}, {
timestamps: true
});
// Indexes
recentPostSchema.index({ createdAt: -1 });
recentPostSchema.index({ originalPostId: 1 });
// Remove __v from JSON output
recentPostSchema.set('toJSON', {
transform: function(doc, ret) {
delete ret.__v;
return ret;
}
});
// Static method to sync with Blog posts
recentPostSchema.statics.syncFromBlogs = async function(limit = 5) {
const Blog = require('./blog');
// Get recent published blogs
const recentBlogs = await Blog.find({ status: 'published' })
.sort({ createdAt: -1 })
.limit(limit)
.select('title slug featuredImage publishedAt');
// Clear existing recent posts
await this.deleteMany({});
// Create new recent posts
const recentPosts = recentBlogs.map(blog => ({
title: blog.title,
slug: blog.slug,
thumbnail: blog.featuredImage,
publishedAt: blog.publishedAt,
originalPostId: blog._id
}));
if (recentPosts.length > 0) {
await this.insertMany(recentPosts);
}
return recentPosts;
};
// Static method to get recent posts
recentPostSchema.statics.getRecent = function(limit = 5) {
return this.find({}).sort({ createdAt: -1 }).limit(limit);
};
module.exports = mongoose.model('RecentPost', recentPostSchema);

View File

@@ -1,76 +0,0 @@
const mongoose = require("mongoose");
// Schema cho hero section
const safetySchema = new mongoose.Schema(
{
//hero section
hero: {
banner: String,
title: String,
},
//approach section
approach: {
badge: String,
title:String,
description:String,
imgs:{
img1:String,
img2:String
},
stats:{
count:String,
label:String,
avatars:[String]
},
features:[
{text:String}
],
cards: [
{
title: String,
content: String,
},
],
},
//philosophy section
philosophy: {
title: String,
subtitle: String,
cards: [
{
title: String,
content: String,
author: {
avt: String,
name: String,
role: String,
rating: String,
},
},
],
},
//security section
security: {
title: String,
subtitle: String,
cards: [
{
title: String,
content: String,
author: {
avt: String,
name: String,
role: String,
rating: String,
},
},
],
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Safety", safetySchema);

View File

@@ -1,118 +0,0 @@
const mongoose = require("mongoose");
// Define sub-schemas first
const authorSchema = new mongoose.Schema(
{
name: String,
type: String,
},
{ _id: false },
);
const clientReviewSchema = new mongoose.Schema(
{
id: String,
rating: Number,
content: String,
author: authorSchema,
icon: String,
},
{ _id: false },
);
const featureSchema = new mongoose.Schema(
{
title: String,
description: String,
},
{ _id: false },
);
const faqSchema = new mongoose.Schema(
{
id: String,
question: String,
answer: String,
isExpanded: { type: Boolean, default: false },
},
{ _id: false },
);
const serviceDetailsSchema = new mongoose.Schema(
{
title: String,
description: String,
mainImage: String,
overviewTitle: String,
overviewDescription: String,
additionalDescription: String,
keyFeaturesTitle: String,
keyFeaturesImage: String,
features: [featureSchema],
faqTitle: String,
faqImage: String,
faq: [faqSchema],
},
{ _id: false },
);
// Main service page schema
const serviceSchema = new mongoose.Schema(
{
pageTitle: String,
// Main services section
services: {
title: {
subTitle: String,
mainTitle: String,
},
items: [
{
slug: String,
name: String,
description: String,
image: String,
layout: String,
details: serviceDetailsSchema,
},
],
},
// Destination countries section
destinations: {
backgroundImage: String,
title: {
subTitle: String,
mainTitle: String,
},
},
// Visa types section
visas: {
items: [
{
id: String,
number: String,
name: String,
description: String,
buttonText: String,
buttonLink: String,
},
],
},
// Client reviews section
reviews: {
title: {
subTitle: String,
mainTitle: String,
},
thumb: String,
items: [clientReviewSchema],
},
},
{ timestamps: true },
);
module.exports = mongoose.model("Service", serviceSchema);

View File

@@ -1,519 +0,0 @@
// models/terms.js
const mongoose = require("mongoose");
// Schema cho content items
const contentItemSchema = new mongoose.Schema(
{
type: {
type: String,
enum: ["paragraph", "section", "header", "list", "cancellation_table", "cancellation_section", "note", "embed", "image"],
required: true,
},
text: {
type: String,
trim: true,
default: "",
},
// Header level (h2, h3, h4, h5, h6)
level: {
type: Number,
min: 1,
max: 6,
default: 2,
},
title: {
type: String,
trim: true,
default: "",
},
content: {
type: String,
trim: true,
default: "",
},
subsections: {
type: [mongoose.Schema.Types.Mixed], // Recursive reference
default: [],
},
items: {
type: [String],
default: [],
},
// List style (for list type)
style: {
type: String,
enum: ["ordered", "unordered"],
default: "unordered",
},
// Embed/video fields (optional)
embed: {
type: String,
trim: true,
default: ''
},
url: {
type: String,
trim: true,
default: ''
},
source: {
type: String,
trim: true,
default: ''
},
videoId: {
type: String,
trim: true,
default: ''
},
caption: {
type: String,
trim: true,
default: ''
},
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
},
{ _id: false }
);
// Schema cho overlay style
const overlayStyleSchema = new mongoose.Schema(
{
backgroundColor: {
type: String,
trim: true,
default: "rgba(0, 0, 0, 0)",
},
},
{ _id: false }
);
// Schema cho hero section - CẤU TRÚC MỚI
const heroSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
default: "Frequently Asked Questions",
},
backgroundImage: {
type: String,
trim: true,
default: "/uploads/terms/faqimage.jpg",
},
sectionClass: {
type: String,
trim: true,
default: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
},
backgroundClasses: {
type: String,
trim: true,
default: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
},
overlayStyle: {
type: overlayStyleSchema,
default: () => ({ backgroundColor: "rgba(0, 0, 0, 0)" }),
},
titleClass: {
type: String,
trim: true,
default: "text-white text-[5vw] uk-text-center",
},
enableScrollspy: {
type: Boolean,
default: true,
},
},
{ _id: false }
);
// Schema cho page section - CẤU TRÚC MỚI
const pageSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
default: "Terms & Conditions Go and Grow Camp e.K.",
},
divider: {
type: Boolean,
default: true,
},
sectionClass: {
type: String,
trim: true,
default: "uk-section-default uk-section-overlap uk-section",
},
titleClass: {
type: String,
trim: true,
default: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
},
dividerClass: {
type: String,
trim: true,
default: "uk-divider-small uk-text-left@m uk-text-center",
},
},
{ _id: false }
);
// Schema cho content section - CẤU TRÚC MỚI
const contentSchema = new mongoose.Schema(
{
sectionClass: {
type: String,
trim: true,
default: "uk-section-muted uk-section-overlap uk-section",
},
textClass: {
type: String,
trim: true,
default: "uk-panel uk-margin text-[1vw]",
},
content: {
type: [contentItemSchema],
default: [],
},
},
{ _id: false }
);
// Main Terms Schema - CẤU TRÚC MỚI
const termsSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
// CHỈ CÒN 3 PHẦN CHÍNH
hero: {
type: heroSchema,
required: true,
},
page: {
type: pageSchema,
required: true,
},
content: {
type: contentSchema,
required: true,
},
language: {
type: String,
default: "en",
},
version: {
type: String,
default: "2.0.0", // Tăng version vì cấu trúc thay đổi
},
isActive: {
type: Boolean,
default: true,
},
migratedFromOldStructure: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);
// Static method: Lấy terms default - CẬP NHẬT THEO CẤU TRÚC MỚI
termsSchema.statics.getDefault = async function(language = "en") {
try {
let terms = await this.findOne({ name: "default", language: language });
if (!terms) {
// Tạo terms mặc định theo cấu trúc mới
terms = new this({
name: "default",
language: language,
hero: {
title: "Frequently Asked Questions",
subtitle: "Our Terms & Conditions",
backgroundImage: "/uploads/terms/faqimage.jpg",
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
overlayStyle: {
backgroundColor: "rgba(0, 0, 0, 0)"
},
titleClass: "text-white text-[5vw] uk-text-center",
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
enableScrollspy: true
},
page: {
title: "Terms & Conditions Go and Grow Camp e.K.",
divider: true,
sectionClass: "uk-section-default uk-section-overlap uk-section",
titleClass: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
dividerClass: "uk-divider-small uk-text-left@m uk-text-center"
},
content: {
sectionClass: "uk-section-muted uk-section-overlap uk-section",
textClass: "uk-panel uk-margin text-[1vw]",
content: [
{
type: "paragraph",
text: "This is an English translation of the original and legally binding German document \"Allgemeine Geschäftsbedingungen Go and Grow Camp e.K.\", which can be viewed at <a href=\"https://www.campadventure.de/de/infos/agb\" target=\"_self\">https://www.campadventure.de/de/infos/agb</a>. This translation is for your information only and is not legally binding."
},
{
type: "paragraph",
text: "<strong>Go and Grow Camp e.K. is the tour operator for individuals, for camps in Germany, England and Northern Ireland.</strong>"
},
{
type: "paragraph",
text: "GUARANTEE: All participants are protected in accordance with the legal regulations governing tour operators in Germany. As per §651, any payments made towards the travel price are insured against insolvency by tourVers."
}
]
},
version: "2.0.0",
isActive: true,
migratedFromOldStructure: false
});
await terms.save();
console.log(`Created default terms for language: ${language} (new structure)`);
}
return terms;
} catch (error) {
console.error("Error in getDefault:", error);
throw error;
}
};
// Method để get terms data
termsSchema.methods.getTermsData = function() {
return this.toObject();
};
// Migration method từ JSON CŨ sang cấu trúc MỚI
termsSchema.statics.migrateFromJson = async function(jsonData, language = "en") {
try {
console.log('Migrating from JSON to new structure...');
// Xóa document cũ nếu có
await this.deleteOne({ name: "default", language: language });
// Chuyển đổi từ cấu trúc cũ sang mới
const processedData = {
name: "default",
language: language,
version: "2.0.0",
isActive: true,
migratedFromOldStructure: true,
hero: {
title: jsonData.hero?.title || "Go and Grow Camp",
subtitle: jsonData.hero?.subtitle || "Our Terms & Conditions",
backgroundImage: jsonData.hero?.backgroundImage || "/uploads/terms/faqimage.jpg",
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
overlayStyle: {
backgroundColor: jsonData.hero?.overlayColor || "rgba(0, 0, 0, 0)"
},
titleClass: "text-white text-[5vw] uk-text-center",
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
enableScrollspy: jsonData.hero?.enableScrollspy || true
},
page: {
title: jsonData.termsHeader?.title || "Terms & Conditions Go and Grow Camp e.K.",
divider: jsonData.termsHeader?.divider !== false,
sectionClass: jsonData.termsHeader?.sectionClass || "uk-section-default uk-section-overlap uk-section",
titleClass: jsonData.termsHeader?.titleClass || "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
dividerClass: jsonData.termsHeader?.dividerClass || "uk-divider-small uk-text-left@m uk-text-center"
},
content: {
sectionClass: jsonData.layout?.termsSectionClass || "uk-section-muted uk-section-overlap uk-section",
textClass: jsonData.layout?.textContentClass || "uk-panel uk-margin text-[1vw]",
content: []
}
};
// Chuyển đổi sections cũ sang content mới
const contentItems = [];
// Thêm disclaimer đầu tiên nếu có
if (jsonData.disclaimer?.text) {
contentItems.push({
type: "paragraph",
text: jsonData.disclaimer.text
});
}
if (jsonData.disclaimer?.importantNote) {
contentItems.push({
type: "paragraph",
text: `<strong>${jsonData.disclaimer.importantNote}</strong>`
});
}
if (jsonData.disclaimer?.legalNote) {
contentItems.push({
type: "paragraph",
text: jsonData.disclaimer.legalNote
});
}
// Thêm disclaimer note
if (jsonData.disclaimer?.note) {
contentItems.push({
type: "paragraph",
text: jsonData.disclaimer.note
});
}
// Thêm các sections
if (jsonData.sections && Array.isArray(jsonData.sections)) {
jsonData.sections.forEach(section => {
if (section.title && section.content) {
const contentItem = {
type: "section",
title: section.title,
content: section.content
};
// Thêm subsections nếu có
if (section.subsections && section.subsections.length > 0) {
contentItem.subsections = section.subsections.map(sub => ({
type: "note",
text: sub.content || sub
}));
}
// Thêm cancellation fees nếu có
if (section.fees) {
contentItem.subsections = contentItem.subsections || [];
// Individual fees
if (section.fees.individual && section.fees.individual.length > 0) {
contentItem.subsections.push({
type: "cancellation_table",
title: "Standard Cancellation Fees",
items: section.fees.individual.map(fee => `${fee.period} ${fee.fee}`)
});
}
// School group fees
if (section.fees.schoolGroups && section.fees.schoolGroups.fees) {
contentItem.subsections.push({
type: "cancellation_section",
title: "Cancellation policy for school groups:",
items: [
section.fees.schoolGroups.freeCorrection,
...section.fees.schoolGroups.fees.map(fee => `${fee.period}: ${fee.fee}`)
]
});
}
// Fee note
if (section.fees.note) {
contentItem.subsections.push({
type: "note",
text: section.fees.note
});
}
}
contentItems.push(contentItem);
}
});
}
// Thêm footer note nếu có
if (jsonData.footerNote?.text) {
contentItems.push({
type: "paragraph",
text: jsonData.footerNote.text
});
}
// Gán content items đã chuyển đổi
processedData.content.content = contentItems;
// Tạo document mới
const newTerms = await this.create(processedData);
console.log(`Terms data migrated to new structure for language: ${language}`);
console.log(`Total content items: ${contentItems.length}`);
return newTerms;
} catch (error) {
console.error("Error migrating terms data to new structure:", error);
throw error;
}
};
// Migration method từ cấu trúc MỚI sang cấu trúc MỚI (dành cho JSON mới)
termsSchema.statics.migrateFromNewJson = async function(jsonData, language = "en") {
try {
console.log('Migrating from new JSON structure...');
// Xóa document cũ nếu có
await this.deleteOne({ name: "default", language: language });
// Tạo document mới với cấu trúc mới
const newTerms = await this.create({
name: "default",
language: language,
version: "2.0.0",
isActive: true,
migratedFromOldStructure: false,
hero: {
title: jsonData.hero?.title || "Go and Grow Camp",
subtitle: jsonData.hero?.subtitle || "Our Terms & Conditions",
backgroundImage: jsonData.hero?.backgroundImage || "/uploads/terms/faqimage.jpg",
sectionClass: jsonData.hero?.sectionClass || "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
backgroundClasses: jsonData.hero?.backgroundClasses || "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
overlayStyle: jsonData.hero?.overlayStyle || { backgroundColor: "rgba(0, 0, 0, 0)" },
titleClass: jsonData.hero?.titleClass || "text-white text-[5vw] uk-text-center",
subtitleClass: jsonData.hero?.subtitleClass || "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
enableScrollspy: jsonData.hero?.enableScrollspy !== undefined ? jsonData.hero.enableScrollspy : true
},
page: {
title: jsonData.page?.title || "Terms & Conditions Go and Grow Camp e.K.",
divider: jsonData.page?.divider !== undefined ? jsonData.page.divider : true,
sectionClass: jsonData.page?.sectionClass || "uk-section-default uk-section-overlap uk-section",
titleClass: jsonData.page?.titleClass || "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
dividerClass: jsonData.page?.dividerClass || "uk-divider-small uk-text-left@m uk-text-center"
},
content: {
sectionClass: jsonData.content?.sectionClass || "uk-section-muted uk-section-overlap uk-section",
textClass: jsonData.content?.textClass || "uk-panel uk-margin text-[1vw]",
content: jsonData.content?.content || []
}
});
console.log(`Terms data created with new structure for language: ${language}`);
console.log(`Hero title: ${newTerms.hero.title}`);
console.log(`Page title: ${newTerms.page.title}`);
console.log(`Content items: ${newTerms.content.content.length}`);
return newTerms;
} catch (error) {
console.error("Error creating terms data from new structure:", error);
throw error;
}
};
const Terms = mongoose.model("Terms", termsSchema);
module.exports = Terms;

View File

@@ -1,45 +0,0 @@
const mongoose = require("mongoose");
const travelSchema = new mongoose.Schema(
{
page: {
title: {
type: String,
default: "Travel Information",
},
description: {
type: String,
default: "",
},
year: {
type: String,
default: "",
},
metadata: {
title: String,
description: String,
},
},
hero: {
title: {
type: String,
default: "Travel Information",
},
backgroundImage: {
type: String,
default: "",
},
},
content: {
type: mongoose.Schema.Types.Mixed,
default: { blocks: [] },
},
enableScrollspy: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);
module.exports = mongoose.model("Travel", travelSchema);

View File

@@ -1,234 +0,0 @@
// 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(
{
// Không dùng `index: true` ở đây vì đã tạo index riêng cho hero.summaryList.* bên dưới
id: { type: Number, required: true },
name: { type: String, required: true },
slug: { type: String, required: 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);