feat: add programme module and blog integration

- Add Programme Mongoose model with sub-schemas and migrateFromJson static method
- Add programme CRUD controller with audit logging and icon sanitization
- Add programme admin views (index, edit) with tabbed form and dynamic arrays
- Add seed migration script for programmes
- Add /api/programmes and /api/programmes/:id public API endpoints
- Register programme routes in admin and public router
- Update dashboard with Programmes card and API endpoint entries
- Update admin sidebar layout to include Programmes nav link
This commit is contained in:
2026-04-20 18:40:29 +07:00
parent ac5fa14c97
commit 30b90051c9
12 changed files with 1311 additions and 2 deletions
+78
View File
@@ -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);