forked from UKSOURCE/cms.lams
Merge branch 'develop' into fea/dat-20042026-CMS-Partnerships-Accreditation-History-Admissions-Policies
This commit is contained in:
+131
-364
@@ -1,422 +1,189 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// Schema cho hero section
|
||||
// ─────────────────────────────────────────────
|
||||
// Sub-schemas
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
const metadataSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, trim: true, default: "" },
|
||||
description: { type: String, trim: true, default: "" },
|
||||
keywords: { type: String, trim: true, default: "" },
|
||||
ogImage: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
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",
|
||||
},
|
||||
badge: { type: String, trim: true, default: "" },
|
||||
titleMain: { type: String, trim: true, default: "" },
|
||||
titleHighlight: { type: String, trim: true, default: "" },
|
||||
description: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho contact card
|
||||
const contactCardSchema = new mongoose.Schema(
|
||||
const channelSchema = 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",
|
||||
},
|
||||
icon: { type: String, trim: true, default: "" },
|
||||
title: { type: String, trim: true, default: "" },
|
||||
detail: { type: String, trim: true, default: "" },
|
||||
subDetail: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho map coordinates
|
||||
const coordinatesSchema = new mongoose.Schema(
|
||||
const supportHourSchema = new mongoose.Schema(
|
||||
{
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
day: { type: String, trim: true, default: "" },
|
||||
time: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho tile layer
|
||||
const tileLayerSchema = new mongoose.Schema(
|
||||
const infoCardsSchema = new mongoose.Schema(
|
||||
{
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
contactInfo: {
|
||||
title: { type: String, trim: true, default: "" },
|
||||
channels: { type: [channelSchema], default: [] },
|
||||
},
|
||||
attribution: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: "",
|
||||
},
|
||||
maxZoom: {
|
||||
type: Number,
|
||||
default: 18,
|
||||
},
|
||||
minZoom: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
supportHours: {
|
||||
title: { type: String, trim: true, default: "" },
|
||||
hours: { type: [supportHourSchema], default: [] },
|
||||
footer: {
|
||||
text: { type: String, trim: true, default: "" },
|
||||
linkText: { type: String, trim: true, default: "" },
|
||||
linkHref: { type: String, trim: true, default: "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho map
|
||||
const mapSchema = new mongoose.Schema(
|
||||
const formOptionSchema = 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,
|
||||
},
|
||||
label: { type: String, trim: true, default: "" },
|
||||
value: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho form field
|
||||
const formFieldSchema = new mongoose.Schema(
|
||||
const simpleFieldSchema = 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: "",
|
||||
},
|
||||
name: { type: String, trim: true, default: "" },
|
||||
label: { type: String, trim: true, default: "" },
|
||||
placeholder: { type: String, trim: true, default: "" },
|
||||
required: { type: Boolean, default: false },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Schema cho submit button
|
||||
const submitButtonSchema = new mongoose.Schema(
|
||||
const inquiryFieldSchema = 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",
|
||||
},
|
||||
name: { type: String, trim: true, default: "" },
|
||||
label: { type: String, trim: true, default: "" },
|
||||
placeholder: { type: String, trim: true, default: "" },
|
||||
required: { type: Boolean, default: true },
|
||||
options: { type: [formOptionSchema], default: [] },
|
||||
},
|
||||
{ _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: "",
|
||||
},
|
||||
heading: { type: String, trim: true, default: "" },
|
||||
description: { type: String, trim: true, default: "" },
|
||||
fields: {
|
||||
type: [formFieldSchema],
|
||||
default: [],
|
||||
},
|
||||
submitButton: {
|
||||
type: submitButtonSchema,
|
||||
required: true,
|
||||
firstName: { type: simpleFieldSchema, default: () => ({}) },
|
||||
lastName: { type: simpleFieldSchema, default: () => ({}) },
|
||||
email: { type: simpleFieldSchema, default: () => ({}) },
|
||||
phone: { type: simpleFieldSchema, default: () => ({}) },
|
||||
inquiryType: { type: inquiryFieldSchema, default: () => ({}) },
|
||||
message: { type: simpleFieldSchema, default: () => ({}) },
|
||||
},
|
||||
submitLabel: { type: String, trim: true, default: "Send Message" },
|
||||
successMessage: { type: String, trim: true, default: "" },
|
||||
errorMessage: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Main Contact Schema
|
||||
const contactSchema = new mongoose.Schema(
|
||||
const faqItemSchema = 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,
|
||||
},
|
||||
question: { type: String, trim: true, default: "" },
|
||||
answer: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// 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",
|
||||
};
|
||||
const faqSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, trim: true, default: "" },
|
||||
subtitle: { type: String, trim: true, default: "" },
|
||||
items: { type: [faqItemSchema], default: [] },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// Tạo migration script để import dữ liệu từ contact-data.json
|
||||
const ctaButtonSchema = new mongoose.Schema(
|
||||
{
|
||||
label: { type: String, trim: true, default: "" },
|
||||
href: { type: String, trim: true, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const ctaSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, trim: true, default: "" },
|
||||
description: { type: String, trim: true, default: "" },
|
||||
primaryButton: { type: ctaButtonSchema, default: () => ({}) },
|
||||
secondaryButton: { type: ctaButtonSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Main Contact Schema (slug-based, same pattern
|
||||
// as StudentSupport / RequestInfo)
|
||||
// ─────────────────────────────────────────────
|
||||
const contactSchema = new mongoose.Schema(
|
||||
{
|
||||
slug: { type: String, default: "contact", unique: true },
|
||||
metadata: { type: metadataSchema, default: () => ({}) },
|
||||
hero: { type: heroSchema, default: () => ({}) },
|
||||
infoCards: { type: infoCardsSchema, default: () => ({}) },
|
||||
form: { type: formSchema, default: () => ({}) },
|
||||
faq: { type: faqSchema, default: () => ({}) },
|
||||
cta: { type: ctaSchema, default: () => ({}) },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Migration helper (idempotent upsert)
|
||||
// ─────────────────────────────────────────────
|
||||
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" });
|
||||
const payload = {
|
||||
metadata: jsonData.metadata || {},
|
||||
hero: jsonData.hero || {},
|
||||
infoCards: jsonData.infoCards || {},
|
||||
form: jsonData.form || {},
|
||||
faq: jsonData.faq || {},
|
||||
cta: jsonData.cta || {},
|
||||
};
|
||||
|
||||
// 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;
|
||||
const existing = await this.findOne({ slug: "contact" });
|
||||
if (existing) {
|
||||
existing.set(payload);
|
||||
await existing.save();
|
||||
console.log("✅ Contact data updated via migration.");
|
||||
return existing;
|
||||
} else {
|
||||
const created = await this.create({ slug: "contact", ...payload });
|
||||
console.log("✅ Contact data created via migration.");
|
||||
return created;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const courseSchema = new mongoose.Schema(
|
||||
{
|
||||
id: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
icon: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const outcomeSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
icon: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const faqSchema = new mongoose.Schema(
|
||||
{
|
||||
question: { type: String, default: "" },
|
||||
answer: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const programmeSchema = new mongoose.Schema(
|
||||
{
|
||||
id: { type: String, required: true, unique: true, trim: true },
|
||||
level: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
duration: { type: String, default: "" },
|
||||
cost: { type: String, default: "" },
|
||||
selected: { type: Boolean, default: false },
|
||||
link: { type: String, default: "" },
|
||||
shortName: { type: String, default: "" },
|
||||
detailBadge: { type: String, default: "" },
|
||||
detailTitle: { type: String, default: "" },
|
||||
detailDescription: { type: String, default: "" },
|
||||
heroImage: { type: String, default: "" },
|
||||
overview: { type: String, default: "" },
|
||||
credits: { type: Number, default: 0 },
|
||||
format: { type: String, default: "100% Online" },
|
||||
nextStartDate: { type: String, default: "" },
|
||||
monthlyCost: { type: String, default: "" },
|
||||
perCourseCost: { type: String, default: "" },
|
||||
coreCourses: { type: [courseSchema], default: [] },
|
||||
electives: { type: [String], default: [] },
|
||||
outcomes: { type: [outcomeSchema], default: [] },
|
||||
faqs: { type: [faqSchema], default: [] },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Upsert from a JSON array. Used by migration scripts.
|
||||
*/
|
||||
programmeSchema.statics.migrateFromJson = async function (jsonArray) {
|
||||
const results = [];
|
||||
for (const item of jsonArray) {
|
||||
const existing = await this.findOne({ id: item.id });
|
||||
if (existing) {
|
||||
existing.set(item);
|
||||
await existing.save();
|
||||
results.push({ action: "updated", id: item.id });
|
||||
} else {
|
||||
await this.create(item);
|
||||
results.push({ action: "created", id: item.id });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("Programme", programmeSchema);
|
||||
@@ -0,0 +1,138 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
// ── Sub-schemas ──────────────────────────────────────────────────────────────
|
||||
|
||||
const metadataSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
keywords: { type: String, default: "" },
|
||||
ogImage: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const valuePropSchema = new mongoose.Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const heroSchema = new mongoose.Schema(
|
||||
{
|
||||
badge: { type: String, default: "" },
|
||||
titleMain: { type: String, default: "" },
|
||||
titleHighlight: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
valueProps: { type: [valuePropSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const formOptionSchema = new mongoose.Schema(
|
||||
{
|
||||
label: { type: String, default: "" },
|
||||
value: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const formFieldSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
placeholder: { type: String, default: "" },
|
||||
required: { type: Boolean, default: false },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const formSelectFieldSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
placeholder: { type: String, default: "" },
|
||||
required: { type: Boolean, default: false },
|
||||
options: { type: [formOptionSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const formTimelineFieldSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, default: "" },
|
||||
label: { type: String, default: "" },
|
||||
options: { type: [formOptionSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const formFieldsSchema = new mongoose.Schema(
|
||||
{
|
||||
firstName: { type: formFieldSchema, default: () => ({}) },
|
||||
lastName: { type: formFieldSchema, default: () => ({}) },
|
||||
email: { type: formFieldSchema, default: () => ({}) },
|
||||
phone: { type: formFieldSchema, default: () => ({}) },
|
||||
program: { type: formSelectFieldSchema, default: () => ({}) },
|
||||
timeline: { type: formTimelineFieldSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const formSchema = new mongoose.Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
fields: { type: formFieldsSchema, default: () => ({}) },
|
||||
consentText: { type: String, default: "" },
|
||||
submitLabel: { type: String, default: "" },
|
||||
footerText: { type: String, default: "" },
|
||||
successMessage: { type: String, default: "" },
|
||||
errorMessage: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
// ── Root schema ───────────────────────────────────────────────────────────────
|
||||
|
||||
const requestInfoSchema = new mongoose.Schema(
|
||||
{
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
default: "request-info",
|
||||
},
|
||||
metadata: { type: metadataSchema, default: () => ({}) },
|
||||
hero: { type: heroSchema, default: () => ({}) },
|
||||
form: { type: formSchema, default: () => ({}) },
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* Seed / migrate từ file JSON (cùng shape với API frontend).
|
||||
* Idempotent — chạy lại không tạo trùng.
|
||||
*/
|
||||
requestInfoSchema.statics.migrateFromJson = async function (jsonData) {
|
||||
const slug = "request-info";
|
||||
const payload = {
|
||||
slug,
|
||||
metadata: jsonData.metadata || {},
|
||||
hero: jsonData.hero || {},
|
||||
form: jsonData.form || {},
|
||||
};
|
||||
|
||||
const existing = await this.findOne({ slug });
|
||||
if (existing) {
|
||||
existing.set(payload);
|
||||
await existing.save();
|
||||
return existing;
|
||||
}
|
||||
return this.create(payload);
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("RequestInfo", requestInfoSchema);
|
||||
@@ -0,0 +1,148 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const ctaButtonSchema = new mongoose.Schema(
|
||||
{
|
||||
label: { type: String, default: "" },
|
||||
href: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const heroSchema = new mongoose.Schema(
|
||||
{
|
||||
badge: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
primaryButton: { type: ctaButtonSchema, default: () => ({}) },
|
||||
secondaryButton: { type: ctaButtonSchema, default: () => ({}) },
|
||||
image: { type: String, default: "" },
|
||||
imageAlt: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const directoryServiceSchema = new mongoose.Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
hours: { type: String, default: "" },
|
||||
hoursIcon: { type: String, default: "" },
|
||||
buttonLabel: { type: String, default: "" },
|
||||
buttonHref: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const directorySchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
subtitle: { type: String, default: "" },
|
||||
services: { type: [directoryServiceSchema], default: [] },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const channelSchema = new mongoose.Schema(
|
||||
{
|
||||
icon: { type: String, default: "" },
|
||||
title: { type: String, default: "" },
|
||||
detail: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const labelPlaceholderSchema = new mongoose.Schema(
|
||||
{
|
||||
label: { type: String, default: "" },
|
||||
placeholder: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const departmentFieldSchema = new mongoose.Schema(
|
||||
{
|
||||
label: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const supportFormFieldsSchema = new mongoose.Schema(
|
||||
{
|
||||
firstName: { type: labelPlaceholderSchema, default: () => ({}) },
|
||||
lastName: { type: labelPlaceholderSchema, default: () => ({}) },
|
||||
studentId: { type: labelPlaceholderSchema, default: () => ({}) },
|
||||
department: { type: departmentFieldSchema, default: () => ({}) },
|
||||
message: { type: labelPlaceholderSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const contactFormSchema = new mongoose.Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
fields: { type: supportFormFieldsSchema, default: () => ({}) },
|
||||
departments: { type: [String], default: [] },
|
||||
submitLabel: { type: String, default: "" },
|
||||
successMessage: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const contactSectionSchema = new mongoose.Schema(
|
||||
{
|
||||
heading: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
channels: { type: [channelSchema], default: [] },
|
||||
form: { type: contactFormSchema, default: () => ({}) },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const metadataSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, default: "" },
|
||||
description: { type: String, default: "" },
|
||||
},
|
||||
{ _id: false },
|
||||
);
|
||||
|
||||
const studentSupportSchema = new mongoose.Schema(
|
||||
{
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
default: "student-support",
|
||||
},
|
||||
metadata: { type: metadataSchema, default: () => ({}) },
|
||||
hero: { type: heroSchema, default: () => ({}) },
|
||||
directory: { type: directorySchema, default: () => ({}) },
|
||||
contactSection: { type: contactSectionSchema, default: () => ({}) },
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
/**
|
||||
* Seed / migrate từ file JSON (cùng shape với API frontend).
|
||||
*/
|
||||
studentSupportSchema.statics.migrateFromJson = async function (jsonData) {
|
||||
const slug = "student-support";
|
||||
const payload = {
|
||||
slug,
|
||||
metadata: jsonData.metadata || {},
|
||||
hero: jsonData.hero || {},
|
||||
directory: jsonData.directory || { services: [] },
|
||||
contactSection: jsonData.contactSection || {},
|
||||
};
|
||||
|
||||
const existing = await this.findOne({ slug });
|
||||
if (existing) {
|
||||
existing.set(payload);
|
||||
await existing.save();
|
||||
return existing;
|
||||
}
|
||||
return this.create(payload);
|
||||
};
|
||||
|
||||
module.exports = mongoose.model("StudentSupport", studentSupportSchema);
|
||||
Reference in New Issue
Block a user