forked from UKSOURCE/cms.hailearning.edu.vn
Merge pull request 'feat/huy-05022026-cms-add-footer-api-management' (#27) from feat/huy-05022026-cms-add-footer-api-management into main
Reviewed-on: UKSOURCE/cms.hailearning.edu.vn#27
This commit is contained in:
383
models/footer.js
383
models/footer.js
@@ -1,234 +1,213 @@
|
||||
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(
|
||||
{
|
||||
platform: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
{
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho footer links
|
||||
const footerLinkSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
// Schema cho phone
|
||||
const phoneSchema = new mongoose.Schema(
|
||||
{
|
||||
display: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho footer columns
|
||||
const footerColumnSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
links: {
|
||||
type: [footerLinkSchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho logo
|
||||
const logoSchema = new mongoose.Schema(
|
||||
{
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
{
|
||||
src: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: "/",
|
||||
},
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho address
|
||||
const addressSchema = new mongoose.Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
// Thêm address2 (địa chỉ dòng 2) không bắt buộc
|
||||
address2: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
// Optional secondary link (e.g., a second map or external resource)
|
||||
link2: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
mapUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho contact info
|
||||
const contactInfoSchema = new mongoose.Schema(
|
||||
{
|
||||
phone: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
hours: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho copyright
|
||||
const copyrightSchema = new mongoose.Schema(
|
||||
{
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
{
|
||||
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 }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Schema cho about section (column đầu tiên)
|
||||
const aboutSchema = new mongoose.Schema(
|
||||
{
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
// 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: [],
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
mapLink: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// Main Footer Schema
|
||||
// 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(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
default: "default",
|
||||
unique: true,
|
||||
{
|
||||
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: [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
about: {
|
||||
type: aboutSchema,
|
||||
required: true,
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
address: {
|
||||
type: addressSchema,
|
||||
required: true,
|
||||
},
|
||||
contact: {
|
||||
type: contactInfoSchema,
|
||||
required: true,
|
||||
},
|
||||
columns: {
|
||||
type: [footerColumnSchema],
|
||||
default: [],
|
||||
},
|
||||
social: {
|
||||
links: {
|
||||
type: [socialLinkSchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
copyright: {
|
||||
type: copyrightSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Tạo migration script để import dữ liệu từ footer.json
|
||||
footerSchema.statics.migrateFromJson = async function (jsonData) {
|
||||
try {
|
||||
// Kiểm tra xem đã có footer mặc định chưa
|
||||
const existingFooter = await this.findOne({ name: "default" });
|
||||
|
||||
if (existingFooter) {
|
||||
// Cập nhật footer hiện có
|
||||
Object.assign(existingFooter, jsonData);
|
||||
await existingFooter.save();
|
||||
console.log("Footer data updated successfully");
|
||||
return existingFooter;
|
||||
} else {
|
||||
// Tạo footer mới với dữ liệu từ JSON
|
||||
const newFooter = await this.create({
|
||||
name: "default",
|
||||
...jsonData,
|
||||
});
|
||||
console.log("Footer data imported successfully");
|
||||
return newFooter;
|
||||
// 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;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error migrating footer data:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("Footer", footerSchema);
|
||||
|
||||
286
models/visa.js
286
models/visa.js
@@ -6,221 +6,221 @@ const mongoose = require("mongoose");
|
||||
|
||||
// VisaItem Schema
|
||||
const VisaItemSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
steps: [VisaProcessStepSchema],
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// VisaCategory Schema
|
||||
const VisaCategorySchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
steps: {
|
||||
type: [[String]],
|
||||
default: [],
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
steps: {
|
||||
type: [[String]],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// VisaService Schema
|
||||
const VisaServiceSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
steps: [VisaProcessStepSchema],
|
||||
},
|
||||
{ _id: false },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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 },
|
||||
{
|
||||
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: "" },
|
||||
{
|
||||
img: { type: String, default: "" },
|
||||
sectionTitle: { type: String, default: "" },
|
||||
helpText: { type: String, default: "" },
|
||||
|
||||
phone: {
|
||||
type: PhoneSchema,
|
||||
default: () => ({}),
|
||||
phone: {
|
||||
type: PhoneSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
email: {
|
||||
type: EmailSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
location: {
|
||||
type: LocationSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
email: {
|
||||
type: EmailSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
location: {
|
||||
type: LocationSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
{ _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,
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
gallery: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
visaCategories: {
|
||||
type: VisaCategorySchema,
|
||||
default: null,
|
||||
},
|
||||
visaService: {
|
||||
type: VisaServiceSchema,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// DetailedView Schema
|
||||
const DetailedViewSchema = new mongoose.Schema(
|
||||
{
|
||||
activeCountry: {
|
||||
type: ActiveCountrySchema,
|
||||
default: null,
|
||||
{
|
||||
activeCountry: {
|
||||
type: ActiveCountrySchema,
|
||||
default: null,
|
||||
},
|
||||
relatedCountries: {
|
||||
type: [RelatedCountrySchema],
|
||||
default: [],
|
||||
},
|
||||
contactInfo: {
|
||||
type: ContactInfoSchema,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
relatedCountries: {
|
||||
type: [RelatedCountrySchema],
|
||||
default: [],
|
||||
},
|
||||
contactInfo: {
|
||||
type: ContactInfoSchema,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
{ _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, unique: true },
|
||||
icon: { type: String, default: "" },
|
||||
services: {
|
||||
type: [String],
|
||||
default: [],
|
||||
{
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
detailedView: {
|
||||
type: DetailedViewSchema,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// ==================== HERO SCHEMA ====================
|
||||
|
||||
const HeroSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "Visa" },
|
||||
summaryList: {
|
||||
type: [VisaCountrySchema],
|
||||
default: [],
|
||||
{
|
||||
title: { type: String, default: "Visa" },
|
||||
summaryList: {
|
||||
type: [VisaCountrySchema],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// ==================== MAIN VISA SCHEMA ====================
|
||||
|
||||
const visaDataSchema = new mongoose.Schema(
|
||||
{
|
||||
hero: {
|
||||
type: HeroSchema,
|
||||
default: () => ({ title: "Visa", summaryList: [] }),
|
||||
{
|
||||
hero: {
|
||||
type: HeroSchema,
|
||||
default: () => ({ title: "Visa", summaryList: [] }),
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// ==================== INDEXES ====================
|
||||
|
||||
Reference in New Issue
Block a user