Adding backend of Student Support, Contact, Request

This commit is contained in:
2026-04-20 16:22:37 +07:00
parent fab72e86a6
commit ac5fa14c97
23 changed files with 3167 additions and 2324 deletions
+131 -364
View File
@@ -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;
}
};