forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
148
models/blog.js
148
models/blog.js
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
32
models/certificate.js
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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
88
models/degree.js
Normal 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);
|
||||
222
models/faq.js
222
models/faq.js
@@ -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;
|
||||
213
models/footer.js
213
models/footer.js
@@ -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);
|
||||
@@ -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);
|
||||
115
models/header.js
115
models/header.js
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
277
models/home.js
277
models/home.js
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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
35
models/qualification.js
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
519
models/terms.js
519
models/terms.js
@@ -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;
|
||||
@@ -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);
|
||||
234
models/visa.js
234
models/visa.js
@@ -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);
|
||||
Reference in New Issue
Block a user