first commit

This commit is contained in:
r2xrzh9q2z-lab
2026-02-02 11:07:09 +07:00
commit d1b931d547
286 changed files with 53992 additions and 0 deletions

64
models/about.js Normal file
View File

@@ -0,0 +1,64 @@
const mongoose = require('mongoose');
const aboutSchema = new mongoose.Schema({
banner: {
image: String,
title: String,
text: String
},
about: {
title: String,
paragraphs: [String],
list_items: [String],
button: {
text: String,
url: String
},
image: String,
quote: {
mark_image: String,
title: String,
text: String,
author: String
}
},
values: {
background_image: String,
items: [{
icon: String,
title: String,
text: String
}]
},
education: {
images: {
student1: String,
student2: String
},
subtitle: String,
title: String,
text: String
},
advantages: {
title: String,
items: [{
number: String,
title: String,
text: String
}]
},
academic_board: {
title: String,
members: [{
image: String,
title: String,
name: String,
color: String
}]
},
updatedAt: Date
}, {
timestamps: true
});
module.exports = mongoose.model('About', aboutSchema);

88
models/aboutUs.js Normal file
View File

@@ -0,0 +1,88 @@
const mongoose = require("mongoose");
const aboutUsSchema = new mongoose.Schema(
{
// Hero section
hero: {
title: String,
backgroundImage: String,
},
// Introduction section with nested services
introduction: {
subtitle: String,
title: String,
description: String,
mainImage: String,
services: [
{
title: String,
description: String,
},
],
},
// Statistics with nested items
statistics: {
items: [
{
number: String,
description: String,
},
],
},
// Accommodation section with nested features
accommodation: {
subtitle: String,
title: String,
description: String,
features: [
{
title: String,
description: String,
icon: String,
},
],
},
// Activities section with nested gallery
activities: {
subtitle: String,
title: String,
description: String,
gallery: [
{
image: String,
title: String,
description: String,
},
],
},
// Newsletter
newsletter: {
imagePath: String,
title: String,
description: String,
buttonText: String,
},
// Events with nested items
events: {
title: String,
items: [
{
imageUrl: String,
date: String,
title: String,
description: String,
age: String,
},
],
},
},
{timestamps: true}
);
module.exports = mongoose.model("AboutUs", aboutUsSchema);

194
models/activity.js Normal file
View File

@@ -0,0 +1,194 @@
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);

106
models/booking.js Normal file
View File

@@ -0,0 +1,106 @@
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);

200
models/bookingSubmission.js Normal file
View File

@@ -0,0 +1,200 @@
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);

245
models/campLocation.js Normal file
View File

@@ -0,0 +1,245 @@
const mongoose = require('mongoose');
// Sub-schemas for better organization
const programOptionSchema = new mongoose.Schema({
_comment: String,
href: String,
imageSrc: String,
title: String,
description: String
}, { _id: false });
const locationSchema = new mongoose.Schema({
_comment: String,
id: {
type: Number,
required: true
},
country: String,
title: String,
imageSrc: String,
imageAlt: String,
readMoreLink: String,
imagePosition: {
type: String,
enum: ['left', 'right']
},
cardSize: {
type: String,
enum: ['default', 'large']
},
scrollspyClass: String,
programOptions: [programOptionSchema]
}, { _id: false });
const campMarkerSchema = new mongoose.Schema({
_comment: String,
id: {
type: Number,
required: true
},
lat: {
type: Number,
required: false // Changed to false since coordinates are optional
},
lng: {
type: Number,
required: false // Changed to false since coordinates are optional
},
title: {
type: String,
required: true
},
image: String
}, { _id: false });
const faqItemSchema = new mongoose.Schema({
_comment: String,
question: {
type: String,
required: true
},
answer: {
type: String,
required: true
}
}, { _id: false });
const securityItemSchema = new mongoose.Schema({
_comment: String,
title: {
type: String,
required: true
},
content: {
type: String,
required: true
}
}, { _id: false });
// Map schemas (similar to contact model)
const coordinatesSchema = new mongoose.Schema({
lat: {
type: Number,
required: true
},
lng: {
type: Number,
required: true
}
}, { _id: false });
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 });
const mapSchema = new mongoose.Schema({
_comment: String,
coordinates: {
type: coordinatesSchema,
required: true
},
zoom: {
type: Number,
default: 15
},
location: {
type: String,
required: true,
trim: true
},
markerTitle: {
type: String,
trim: true,
default: ''
},
tileLayer: {
type: tileLayerSchema,
required: true
}
}, { _id: false });
// Main schema
const campLocationSchema = new mongoose.Schema({
metadata: {
_comment: String,
title: String,
description: String
},
hero: {
_comment: String,
title: String,
backgroundImage: String,
overlayColor: String
},
camps: [campMarkerSchema],
locationsSection: {
_comment: String,
title: String,
readMoreButtonText: {
type: String,
default: 'read more'
}
},
locations: [locationSchema],
intro: {
_comment: String,
content: String
},
map: {
coordinates: {
lat: { type: Number, default: 0 },
lng: { type: Number, default: 0 }
},
zoom: { type: Number, default: 5 },
location: { type: String, default: '' },
markerTitle: { type: String, default: '' },
tileLayer: {
url: { type: String, default: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
attribution: { type: String, default: '' },
maxZoom: { type: Number, default: 18 },
minZoom: { type: Number, default: 0 }
}
},
faqSection: {
_comment: String,
title: String,
buttonText: {
type: String,
default: 'More questions'
},
buttonIcon: {
type: String,
default: 'comments'
},
buttonLink: {
type: String,
default: '/info/faq'
}
},
faq: [faqItemSchema],
welcomeQuote: {
_comment: String,
title: String,
quote: String,
author: String
},
securityConcept: {
_comment: String,
title: String,
introduction: String,
items: [securityItemSchema]
},
updatedAt: Date
}, {
timestamps: true
});
// Indexes for better query performance
campLocationSchema.index({ 'camps.id': 1 });
campLocationSchema.index({ 'locations.id': 1 });
campLocationSchema.index({ 'locations.country': 1 });
// Virtual to get camp by ID
campLocationSchema.virtual('getCampById').get(function() {
return (id) => this.camps.find(camp => camp.id === id);
});
// Virtual to get location by ID
campLocationSchema.virtual('getLocationById').get(function() {
return (id) => this.locations.find(location => location.id === id);
});
// Method to get locations by country
campLocationSchema.methods.getLocationsByCountry = function(country) {
return this.locations.filter(location => location.country === country);
};
// Method to get all countries
campLocationSchema.methods.getAllCountries = function() {
return [...new Set(this.locations.map(location => location.country))];
};
// Static method to get the latest camp location data
campLocationSchema.statics.getLatest = function() {
return this.findOne().sort({ updatedAt: -1 });
};
module.exports = mongoose.model('CampLocation', campLocationSchema);

387
models/contact.js Normal file
View File

@@ -0,0 +1,387 @@
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: "",
},
tileLayer: {
type: tileLayerSchema,
required: true,
},
},
{ _id: false }
);
// Schema cho form field
const formFieldSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
type: {
type: String,
required: true,
trim: true,
enum: ["text", "email", "tel", "textarea", "programme"],
},
placeholder: {
type: String,
trim: true,
default: "",
},
required: {
type: Boolean,
default: false,
},
programmeName: {
type: String,
trim: true,
default: "",
},
},
{ _id: false }
);
// Schema cho submit button
const submitButtonSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho form
const formSchema = new mongoose.Schema(
{
sectionLabel: {
type: String,
trim: true,
default: "",
},
heading: {
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 || "",
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 || "",
fields: (jsonData.form?.fields || []).map((field) => ({
name: field.name || "",
type: field.type || "text",
placeholder: field.placeholder || "",
required: field.required || false,
programmeName: field.programmeName || "",
})),
submitButton: {
text: jsonData.form?.submitButton?.text || "Send Message",
},
},
};
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);

32
models/department.js Normal file
View File

@@ -0,0 +1,32 @@
const mongoose = require('mongoose');
// Helper function to generate slug
function generateSlug(name) {
return name
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/-+/g, '-'); // Replace multiple - with single -
}
const departmentSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
unique: true,
lowercase: true
},
slug: {
type: String,
required: true,
unique: true,
lowercase: true,
index: true
}
}, {
timestamps: false // Thêm timestamps để theo dõi thời gian tạo/cập nhật
});
module.exports = mongoose.model('Department', departmentSchema);

222
models/faq.js Normal file
View File

@@ -0,0 +1,222 @@
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;

234
models/footer.js Normal file
View File

@@ -0,0 +1,234 @@
const mongoose = require("mongoose");
// Schema cho social links
const socialLinkSchema = new mongoose.Schema(
{
platform: {
type: String,
required: true,
trim: true,
},
url: {
type: String,
required: true,
trim: true,
},
icon: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho footer links
const footerLinkSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
url: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho footer columns
const footerColumnSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
links: {
type: [footerLinkSchema],
default: [],
},
},
{ _id: false }
);
// Schema cho logo
const logoSchema = new mongoose.Schema(
{
src: {
type: String,
required: true,
trim: true,
},
alt: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho address
const addressSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
},
// Thêm address2 (địa chỉ dòng 2) không bắt buộc
address2: {
type: String,
trim: true,
default: "",
},
// Optional secondary link (e.g., a second map or external resource)
link2: {
type: String,
trim: true,
default: "",
},
mapUrl: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho contact info
const contactInfoSchema = new mongoose.Schema(
{
phone: {
type: String,
required: true,
trim: true,
},
hours: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho copyright
const copyrightSchema = new mongoose.Schema(
{
text: {
type: String,
required: true,
trim: true,
},
},
{ _id: false }
);
// Schema cho about section (column đầu tiên)
const aboutSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
required: true,
trim: true,
},
mapLink: {
text: {
type: String,
required: true,
trim: true,
},
url: {
type: String,
required: true,
trim: true,
},
},
},
{ _id: false }
);
// Main Footer Schema
const footerSchema = new mongoose.Schema(
{
name: {
type: String,
default: "default",
unique: true,
},
about: {
type: aboutSchema,
required: true,
},
address: {
type: addressSchema,
required: true,
},
contact: {
type: contactInfoSchema,
required: true,
},
columns: {
type: [footerColumnSchema],
default: [],
},
social: {
links: {
type: [socialLinkSchema],
default: [],
},
},
copyright: {
type: copyrightSchema,
required: true,
},
},
{
timestamps: true,
}
);
// Tạo migration script để import dữ liệu từ footer.json
footerSchema.statics.migrateFromJson = async function (jsonData) {
try {
// Kiểm tra xem đã có footer mặc định chưa
const existingFooter = await this.findOne({ name: "default" });
if (existingFooter) {
// Cập nhật footer hiện có
Object.assign(existingFooter, jsonData);
await existingFooter.save();
console.log("Footer data updated successfully");
return existingFooter;
} else {
// Tạo footer mới với dữ liệu từ JSON
const newFooter = await this.create({
name: "default",
...jsonData,
});
console.log("Footer data imported successfully");
return newFooter;
}
} catch (error) {
console.error("Error migrating footer data:", error);
throw error;
}
};
module.exports = mongoose.model("Footer", footerSchema);

51
models/form.js Normal file
View File

@@ -0,0 +1,51 @@
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);

129
models/header.js Normal file
View File

@@ -0,0 +1,129 @@
const mongoose = require('mongoose');
// Schema cho các link trong topbar
const topbarLinkSchema = new mongoose.Schema({
text: {
type: String,
required: true,
trim: true
},
url: {
type: String,
required: true,
trim: true
}
}, { _id: false });
// Schema cho contact info trong topbar
const contactInfoSchema = new mongoose.Schema({
phone: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
}
}, { _id: false });
// Schema cho topbar
const topbarSchema = new mongoose.Schema({
contactInfo: {
type: contactInfoSchema,
required: true
},
links: {
type: [topbarLinkSchema],
default: []
}
}, { _id: false });
// Main Header Schema
const headerSchema = new mongoose.Schema({
name: {
type: String,
default: 'default',
unique: true
},
topbar: {
type: topbarSchema,
required: true
},
logo: {
type: String,
required: true,
trim: true
}
}, {
timestamps: true
});
// Method để lấy menu tree từ collection menuHeader
headerSchema.statics.getMenuTree = async function() {
try {
const Menu = require('./menuHeader');
return await Menu.getMenuTree();
} catch (error) {
console.error('Error getting menu tree:', error);
throw error;
}
};
// Method để lấy menu tree với programmes từ collection menuHeader
headerSchema.statics.getMenuTreeWithProgrammes = async function() {
try {
const Menu = require('./menuHeader');
return await Menu.getMenuTreeWithProgrammes();
} catch (error) {
console.error('Error getting menu tree with programmes:', error);
throw error;
}
};
// Method để lấy programmes theo menu ID
headerSchema.statics.getProgrammesByMenuId = async function(menuId) {
try {
const Menu = require('./menuHeader');
return await Menu.getProgrammesByMenuId(menuId);
} catch (error) {
console.error('Error getting programmes by menu ID:', error);
throw error;
}
};
// Tạo migration script để import dữ liệu từ menu.json
headerSchema.statics.migrateFromJson = async function(jsonData) {
try {
// Kiểm tra xem đã có header mặc định chưa
const existingHeader = await this.findOne({ name: 'default' });
// Chỉ giữ lại topbar và logo, bỏ search và mainMenu
const headerData = {
topbar: jsonData.topbar,
logo: jsonData.logo
};
if (existingHeader) {
// Cập nhật header hiện có
Object.assign(existingHeader, headerData);
await existingHeader.save();
console.log('Header data updated successfully');
return existingHeader;
} else {
// Tạo header mới với dữ liệu từ JSON
const newHeader = await this.create({
name: 'default',
...headerData
});
console.log('Header data imported successfully');
return newHeader;
}
} catch (error) {
console.error('Error migrating header data:', error);
throw error;
}
};
module.exports = mongoose.model('Header', headerSchema);

96
models/hero.js Normal file
View File

@@ -0,0 +1,96 @@
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);

177
models/home.js Normal file
View File

@@ -0,0 +1,177 @@
const mongoose = require('mongoose');
const homeSchema = new mongoose.Schema({
// New structure - Camp data
hero: {
title: { type: String, default: '' },
description: { type: String, default: '' },
backgroundImage: { type: String, default: '' },
button: {
label: { type: String, default: 'Book Your Adventure' },
href: { type: String, default: '/booking' }
},
contactBox: {
welcomeText: { type: String, default: '' },
phone: {
label: { type: String, default: 'Call us' },
number: { type: String, default: '' },
href: { type: String, default: '' }
},
email: {
label: { type: String, default: 'Email' },
address: { type: String, default: '' },
href: { type: String, default: '' }
},
workingHours: {
label: { type: String, default: 'Working Hours' },
hours: { type: String, default: '' }
}
}
},
about: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
description: { type: String, default: '' },
images: {
mainImage1: { type: String, default: '' },
mainImage2: { type: String, default: '' },
avatars: [{ type: String }]
},
features: [{ type: String }],
quote: { type: String, default: '' },
button: {
label: { type: String, default: '' },
href: { type: String, default: '' }
},
stats: {
customerCount: { type: Number, default: 0 },
customerLabel: { type: String, default: '' }
}
},
missionVision: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
backgroundImage: { type: String, default: '' },
cards: [{
title: { type: String, default: '' },
description: { type: String, default: '' }
}]
},
whyChooseUs: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
description: { type: String, default: '' },
button: {
label: { type: String, default: '' },
href: { type: String, default: '' }
},
features: [{
title: { type: String, default: '' },
description: { type: String, default: '' }
}],
tags: [{ type: String }],
cta: {
text: { type: String, default: '' },
linkText: { type: String, default: '' },
linkHref: { type: String, default: '' }
}
},
activities: {
cards: [{
title: { type: String, default: '' },
description: { type: String, default: '' },
image: { type: String, default: '' }
}]
},
faq: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
description: { type: String, default: '' },
image: { type: String, default: '' },
contact: {
title: { type: String, default: '' },
info: { type: String, default: '' }
},
questions: [{
question: { type: String, default: '' },
answer: { type: String, default: '' }
}]
},
partners: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
backgroundImage: { type: String, default: '' },
logos: [{ type: String }],
cta: {
badge: { type: String, default: '' },
text: { type: String, default: '' },
linkText: { type: String, default: '' },
linkHref: { type: String, default: '' }
}
},
programs: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
button: {
label: { type: String, default: '' },
href: { type: String, default: '' }
},
card: {
pricePrefix: { type: String, default: 'from' },
priceSuffix: { type: String, default: 'USD' },
buttonLabel: { type: String, default: 'Camp Detail' },
buttonHref: { type: String, default: '/camp-profiles' }
},
items: [{
id: { type: String, default: '' },
title: { type: String, default: '' },
price: { type: String, default: '' },
seasons: [{ type: String }],
age: { type: String, default: '' },
location: { type: String, default: '' },
image: { type: String, default: '' },
slug: { type: String, default: '' }
}]
},
newsletter: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
description: { type: String, default: '' },
image: { type: String, default: '' },
decorativeImage: { type: String, default: '' },
button: {
label: { type: String, default: '' },
placeholder: { type: String, default: '' },
href: { type: String, default: '' }
}
},
latestPosts: {
title: { type: String, default: '' },
subtitle: { type: String, default: '' },
searchPlaceholder: { type: String, default: '' },
sidebarTitle: { type: String, default: '' },
blogPosts: [{
id: { type: Number },
image: { type: String, default: '' },
title: { type: String, default: '' },
description: { type: String, default: '' },
date: { type: String, default: '' }
}],
sidebarPosts: [{
id: { type: Number },
image: { type: String, default: '' },
title: { type: String, default: '' },
description: { type: String, default: '' }
}],
featuredCard: {
image: { type: String, default: '' },
title: { type: String, default: '' },
description: { type: String, default: '' }
}
}
}, {
timestamps: true
});
module.exports = mongoose.model('Home', homeSchema);

302
models/insurance.js Normal file
View File

@@ -0,0 +1,302 @@
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;

67
models/level.js Normal file
View File

@@ -0,0 +1,67 @@
const mongoose = require('mongoose');
const levelSchema = new mongoose.Schema({
brochure: { type: String },
type: {
type: String,
required: 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);

185
models/menuHeader.js Normal file
View File

@@ -0,0 +1,185 @@
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);

28
models/migration.js Normal file
View File

@@ -0,0 +1,28 @@
const mongoose = require('mongoose');
const migrationSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true,
index: true
},
batch: {
type: Number,
required: true,
default: 1
},
ranAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
// Index để tìm kiếm nhanh
migrationSchema.index({ name: 1 });
migrationSchema.index({ batch: -1 });
module.exports = mongoose.model('Migration', migrationSchema);

76
models/safety.js Normal file
View File

@@ -0,0 +1,76 @@
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);

519
models/terms.js Normal file
View File

@@ -0,0 +1,519 @@
// 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;

45
models/travel.js Normal file
View File

@@ -0,0 +1,45 @@
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);

0
models/visa.js Normal file
View File