Merge pull request 'feat/huy-05022026-cms-add-footer-api-management' (#23) from feat/huy-05022026-cms-add-footer-api-management into main

Reviewed-on: UKSOURCE/cms.hailearning.edu.vn#23
This commit is contained in:
2026-02-05 09:11:13 +00:00
11 changed files with 2673 additions and 1736 deletions

View File

@@ -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);

View File

@@ -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 ====================