forked from UKSOURCE/cms.lams
Merge branch 'develop' into fea/dat-20042026-CMS-Partnerships-Accreditation-History-Admissions-Policies
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
// Hero
|
||||
const HeroSchema = new Schema(
|
||||
{
|
||||
badge: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
studentCount: { type: String, default: "" },
|
||||
image: { type: String, default: "" },
|
||||
imageAlt: { type: String, default: "" },
|
||||
coreValues: { type: [String], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Leadership
|
||||
const SocialSchema = new Schema(
|
||||
{
|
||||
linkedin: { type: String, default: "" },
|
||||
twitter: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const LeadershipMemberSchema = new Schema(
|
||||
{
|
||||
name: { type: String, default: "" },
|
||||
role: { type: String, default: "" },
|
||||
bio: { type: String, default: "" },
|
||||
avatar: { type: String, default: "" },
|
||||
social: { type: SocialSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const LeadershipSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
members: { type: [LeadershipMemberSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Learning Model
|
||||
const LearningFeatureSchema = new Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
desc: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const LearningModeSchema = new Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
features: { type: [LearningFeatureSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const LearningModelSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
async: { type: LearningModeSchema, default: () => ({}) },
|
||||
sync: { type: LearningModeSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Accreditation
|
||||
const AccreditationBadgeSchema = new Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
desc: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const AccreditationStatSchema = new Schema(
|
||||
{
|
||||
value: { type: String, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
isPrimary: { type: Boolean, default: false },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const AccreditationSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
badges: { type: [AccreditationBadgeSchema], default: [] },
|
||||
stats: { type: [AccreditationStatSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Success Stories
|
||||
const StorySchema = new Schema(
|
||||
{
|
||||
quote: { type: String, default: "" },
|
||||
name: { type: String, default: "" },
|
||||
program: { type: String, default: "" },
|
||||
avatar: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const SuccessStoriesSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
stories: { type: [StorySchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// CTA
|
||||
const CtaButtonSchema = new Schema(
|
||||
{
|
||||
label: { type: String, default: "" },
|
||||
href: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const CtaSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
primaryButton: { type: CtaButtonSchema, default: () => ({}) },
|
||||
secondaryButton: { type: CtaButtonSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Root schema
|
||||
const AboutSchema = new Schema(
|
||||
{
|
||||
hero: { type: HeroSchema, default: () => ({}) },
|
||||
leadership: { type: LeadershipSchema, default: () => ({}) },
|
||||
learningModel: { type: LearningModelSchema, default: () => ({}) },
|
||||
accreditation: { type: AccreditationSchema, default: () => ({}) },
|
||||
successStories: { type: SuccessStoriesSchema, default: () => ({}) },
|
||||
cta: { type: CtaSchema, default: () => ({}) },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
strict: false,
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("About", AboutSchema);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
+58
-169
@@ -1,213 +1,102 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho menu links
|
||||
const menuLinkSchema = new mongoose.Schema(
|
||||
const { Schema } = mongoose;
|
||||
|
||||
// Brand
|
||||
const LogoSchema = new Schema(
|
||||
{
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
image: { type: String, default: "" },
|
||||
href: { type: String, default: "/" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho social links
|
||||
const socialLinkSchema = new mongoose.Schema(
|
||||
const SocialItemSchema = new Schema(
|
||||
{
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
icon: { type: String, default: "" },
|
||||
href: { type: String, default: "#" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho phone
|
||||
const phoneSchema = new mongoose.Schema(
|
||||
const BrandSchema = new Schema(
|
||||
{
|
||||
display: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
logo: { type: LogoSchema, default: () => ({}) },
|
||||
description: { type: String, default: "" },
|
||||
social: { type: [SocialItemSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho logo
|
||||
const logoSchema = new mongoose.Schema(
|
||||
// Explore
|
||||
const ExploreLinkSchema = new 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: "/",
|
||||
},
|
||||
label: { type: String, default: "" },
|
||||
href: { type: String, default: "#" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho copyright
|
||||
const copyrightSchema = new mongoose.Schema(
|
||||
const ExploreSchema = new 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.",
|
||||
},
|
||||
heading: { type: String, default: "" },
|
||||
links: { type: [ExploreLinkSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho top section
|
||||
const topSchema = new mongoose.Schema(
|
||||
// Contact
|
||||
const ContactSchema = new 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: [],
|
||||
},
|
||||
heading: { type: String, default: "" },
|
||||
address: { type: String, default: "" },
|
||||
phone: { type: String, default: "" },
|
||||
email: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho bottom section
|
||||
const bottomSchema = new mongoose.Schema(
|
||||
// Newsletter
|
||||
const NewsletterSchema = new Schema(
|
||||
{
|
||||
copyright: {
|
||||
type: copyrightSchema,
|
||||
default: () => ({ text: "Copyright©", brand: "", rights: "All Rights Reserved." }),
|
||||
},
|
||||
menuLinks: {
|
||||
type: [menuLinkSchema],
|
||||
default: [],
|
||||
},
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
placeholder: { type: String, default: "" },
|
||||
buttonText: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Main Footer Schema - khớp 100% với footer.json
|
||||
const footerSchema = new mongoose.Schema(
|
||||
// Bottom
|
||||
const BottomLinkSchema = new 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: [],
|
||||
}),
|
||||
},
|
||||
label: { type: String, default: "" },
|
||||
href: { type: String, default: "#" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const BottomSchema = new Schema(
|
||||
{
|
||||
copyright: { type: String, default: "" },
|
||||
links: { type: [BottomLinkSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Root
|
||||
const FooterSchema = new Schema(
|
||||
{
|
||||
brand: { type: BrandSchema, default: () => ({}) },
|
||||
explore: { type: ExploreSchema, default: () => ({}) },
|
||||
contact: { type: ContactSchema, default: () => ({}) },
|
||||
newsletter: { type: NewsletterSchema, default: () => ({}) },
|
||||
bottom: { type: BottomSchema, default: () => ({}) },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
strict: false,
|
||||
},
|
||||
);
|
||||
|
||||
// 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);
|
||||
module.exports = mongoose.model("Footer", FooterSchema);
|
||||
|
||||
+12
-77
@@ -1,84 +1,7 @@
|
||||
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,
|
||||
@@ -86,6 +9,18 @@ const headerSchema = new mongoose.Schema(
|
||||
alt: String,
|
||||
},
|
||||
|
||||
// Sign In Button
|
||||
signInButton: {
|
||||
label: {
|
||||
type: String,
|
||||
default: "Sign In",
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: "/signin",
|
||||
},
|
||||
},
|
||||
|
||||
// CTA Button
|
||||
ctaButton: {
|
||||
label: String,
|
||||
|
||||
+61
-220
@@ -3,49 +3,41 @@ const mongoose = require("mongoose");
|
||||
const { Schema } = mongoose;
|
||||
|
||||
// Reusable small schemas
|
||||
const LinkSchema = new Schema(
|
||||
const FloatingBadgeSchema = new Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
value: { type: String, default: "" },
|
||||
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)
|
||||
badge: { type: String, default: "" },
|
||||
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: "" },
|
||||
searchPlaceholder: { type: String, default: "" },
|
||||
buttonLabel: { type: String, default: "" },
|
||||
image: { type: String, default: "" },
|
||||
imageAlt: { type: String, default: "" },
|
||||
floatingBadge: { type: FloatingBadgeSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const WhyChooseUsItemSchema = new Schema(
|
||||
const QuickLinkItemSchema = new Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
linkText: { type: String, default: "" },
|
||||
href: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const ValuePropFeatureSchema = new Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
@@ -54,218 +46,68 @@ const WhyChooseUsItemSchema = new Schema(
|
||||
{ _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(
|
||||
const ValuePropStatSchema = new Schema(
|
||||
{
|
||||
value: { type: String, default: "" },
|
||||
suffix: { type: String, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
image: { type: String, default: "" },
|
||||
imageAlt: { 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(
|
||||
const ValuePropSchema = new Schema(
|
||||
{
|
||||
badge: { type: String, default: "" },
|
||||
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: "" },
|
||||
description: { type: String, default: "" },
|
||||
features: { type: [ValuePropFeatureSchema], default: [] },
|
||||
stats: { type: [ValuePropStatSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const BlogPreviewSchema = new Schema(
|
||||
const ProgramItemSchema = new Schema(
|
||||
{
|
||||
category: { type: String, default: "" },
|
||||
image: { type: String, default: "" },
|
||||
duration: { type: String, default: "" },
|
||||
rating: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
studentCount: { type: String, default: "" },
|
||||
href: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const ProgramsSchema = 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' }],
|
||||
description: { type: String, default: "" },
|
||||
items: { type: [ProgramItemSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const RequestInfoSchema = new Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
phone: { type: String, default: "" },
|
||||
email: { type: String, default: "" },
|
||||
programs: { type: [String], default: [] },
|
||||
},
|
||||
{ _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: () => ({}) },
|
||||
quickLinks: { type: [QuickLinkItemSchema], default: [] },
|
||||
valueProp: { type: ValuePropSchema, default: () => ({}) },
|
||||
programs: { type: ProgramsSchema, default: () => ({}) },
|
||||
requestInfo: { type: RequestInfoSchema, default: () => ({}) },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
@@ -274,4 +116,3 @@ const HomeSchema = new Schema(
|
||||
);
|
||||
|
||||
module.exports = mongoose.model("Home", HomeSchema);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
-234
@@ -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