forked from UKSOURCE/cms.lams
fix
This commit is contained in:
@@ -4,6 +4,45 @@ const Programme = require("../models/programme");
|
|||||||
const writeAuditLog = require("../audit/writeAuditLog");
|
const writeAuditLog = require("../audit/writeAuditLog");
|
||||||
const diffObject = require("../audit/diffObject");
|
const diffObject = require("../audit/diffObject");
|
||||||
const AUDIT_ACTIONS = require("../constants/auditAction");
|
const AUDIT_ACTIONS = require("../constants/auditAction");
|
||||||
|
const { readJsonFile, writeJsonFile } = require("../utils/jsonHelper");
|
||||||
|
const DEFAULT_PROGRAMME_FIELDS = [
|
||||||
|
"Computer Science",
|
||||||
|
"Data Analytics",
|
||||||
|
"Business & Admin",
|
||||||
|
"Healthcare",
|
||||||
|
"Finance & Accounting",
|
||||||
|
"Marketing",
|
||||||
|
"Education",
|
||||||
|
"Engineering",
|
||||||
|
"Psychology",
|
||||||
|
"Cybersecurity",
|
||||||
|
];
|
||||||
|
const DEFAULT_DURATION_RANGES = [
|
||||||
|
"< 6 months",
|
||||||
|
"6-12 months",
|
||||||
|
"12-24 months",
|
||||||
|
"24-48 months",
|
||||||
|
"48+ months",
|
||||||
|
];
|
||||||
|
const DEFAULT_DEGREE_LEVELS = ["Bachelor's", "Master's"];
|
||||||
|
|
||||||
|
function readProgrammeFiltersConfig() {
|
||||||
|
const saved = readJsonFile("programme-filters");
|
||||||
|
return {
|
||||||
|
levels:
|
||||||
|
saved && Array.isArray(saved.levels) && saved.levels.length > 0
|
||||||
|
? saved.levels
|
||||||
|
: DEFAULT_DEGREE_LEVELS,
|
||||||
|
fields:
|
||||||
|
saved && Array.isArray(saved.fields) && saved.fields.length > 0
|
||||||
|
? saved.fields
|
||||||
|
: DEFAULT_PROGRAMME_FIELDS,
|
||||||
|
durations:
|
||||||
|
saved && Array.isArray(saved.durations) && saved.durations.length > 0
|
||||||
|
? saved.durations
|
||||||
|
: DEFAULT_DURATION_RANGES,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize all icon fields in the payload against the ICON_OPTIONS whitelist.
|
* Normalize all icon fields in the payload against the ICON_OPTIONS whitelist.
|
||||||
@@ -69,15 +108,24 @@ exports.edit = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
let programme;
|
let programme;
|
||||||
|
const filtersConfig = readProgrammeFiltersConfig();
|
||||||
|
const levelOptions = Array.isArray(filtersConfig.levels)
|
||||||
|
? [...filtersConfig.levels]
|
||||||
|
: [];
|
||||||
|
const durationOptions = Array.isArray(filtersConfig.durations)
|
||||||
|
? [...filtersConfig.durations]
|
||||||
|
: [];
|
||||||
|
|
||||||
if (id === "new") {
|
if (id === "new") {
|
||||||
programme = {
|
programme = {
|
||||||
_id: null,
|
_id: null,
|
||||||
id: "",
|
id: "",
|
||||||
level: "",
|
level: "",
|
||||||
|
fieldOfStudy: "",
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
duration: "",
|
duration: "",
|
||||||
|
durationInMonths: 0,
|
||||||
cost: "",
|
cost: "",
|
||||||
selected: false,
|
selected: false,
|
||||||
link: "",
|
link: "",
|
||||||
@@ -103,11 +151,19 @@ exports.edit = async (req, res) => {
|
|||||||
req.flash("error_msg", "Programme not found");
|
req.flash("error_msg", "Programme not found");
|
||||||
return res.redirect("/admin/programme");
|
return res.redirect("/admin/programme");
|
||||||
}
|
}
|
||||||
|
if (programme.level && !levelOptions.includes(programme.level)) {
|
||||||
|
levelOptions.push(programme.level);
|
||||||
|
}
|
||||||
|
if (programme.duration && !durationOptions.includes(programme.duration)) {
|
||||||
|
durationOptions.push(programme.duration);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render("admin/programme/edit", {
|
res.render("admin/programme/edit", {
|
||||||
title: id === "new" ? "Add New Programme" : `Edit: ${programme.title}`,
|
title: id === "new" ? "Add New Programme" : `Edit: ${programme.title}`,
|
||||||
programme,
|
programme,
|
||||||
|
levelOptions,
|
||||||
|
durationOptions,
|
||||||
iconOptions: ICON_OPTIONS,
|
iconOptions: ICON_OPTIONS,
|
||||||
frontendUrl: process.env.FRONTEND_URL || "",
|
frontendUrl: process.env.FRONTEND_URL || "",
|
||||||
currentPath: req.path,
|
currentPath: req.path,
|
||||||
@@ -145,7 +201,9 @@ exports.update = async (req, res) => {
|
|||||||
title: (body.title || "").trim(),
|
title: (body.title || "").trim(),
|
||||||
description: (body.description || "").trim(),
|
description: (body.description || "").trim(),
|
||||||
duration: (body.duration || "").trim(),
|
duration: (body.duration || "").trim(),
|
||||||
|
durationInMonths: parseInt(body.durationInMonths, 10) || 0,
|
||||||
cost: (body.cost || "").trim(),
|
cost: (body.cost || "").trim(),
|
||||||
|
fieldOfStudy: (body.fieldOfStudy || "").trim(),
|
||||||
selected: body.selected === "true" || body.selected === true,
|
selected: body.selected === "true" || body.selected === true,
|
||||||
link: (body.link || "").trim(),
|
link: (body.link || "").trim(),
|
||||||
shortName: (body.shortName || "").trim(),
|
shortName: (body.shortName || "").trim(),
|
||||||
@@ -272,6 +330,98 @@ exports.api = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/programmes/filters
|
||||||
|
* Return filter options for FE (field of study + duration ranges + levels).
|
||||||
|
*/
|
||||||
|
exports.apiFilters = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const data = await Programme.find().lean();
|
||||||
|
const cfg = readProgrammeFiltersConfig();
|
||||||
|
|
||||||
|
const normalize = (value) =>
|
||||||
|
String(value || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/g, "");
|
||||||
|
const levels = cfg.levels.map((label) => {
|
||||||
|
const normalizedLabel = normalize(label);
|
||||||
|
const count = data.filter((p) =>
|
||||||
|
normalize(p.level).includes(normalizedLabel)
|
||||||
|
).length;
|
||||||
|
return { label, count };
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldsMap = new Map(cfg.fields.map((f) => [f, 0]));
|
||||||
|
data.forEach((p) => {
|
||||||
|
if (p.fieldOfStudy && fieldsMap.has(p.fieldOfStudy)) {
|
||||||
|
fieldsMap.set(p.fieldOfStudy, (fieldsMap.get(p.fieldOfStudy) || 0) + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const fields = [...fieldsMap.entries()].map(([label, count]) => ({ label, count }));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
levels,
|
||||||
|
fields,
|
||||||
|
durations: cfg.durations.map((label) => ({ label })),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("programme.apiFilters:", err);
|
||||||
|
res.status(500).json({ error: "Error loading programme filters" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/programme/filters
|
||||||
|
* CMS page to manage programme filter options centrally.
|
||||||
|
*/
|
||||||
|
exports.filtersPage = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const cfg = readProgrammeFiltersConfig();
|
||||||
|
res.render("admin/programme/filters", {
|
||||||
|
title: "Programme Filters",
|
||||||
|
filters: cfg,
|
||||||
|
frontendUrl: process.env.FRONTEND_URL || "",
|
||||||
|
currentPath: req.path,
|
||||||
|
user: req.session.user,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("programme.filtersPage:", err);
|
||||||
|
req.flash("error_msg", "Error loading programme filters");
|
||||||
|
res.redirect("/admin/programme");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/programme/filters
|
||||||
|
* Save centralized programme filters.
|
||||||
|
*/
|
||||||
|
exports.updateFilters = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const normalizeLines = (text) =>
|
||||||
|
String(text || "")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const levels = normalizeLines(req.body.levels);
|
||||||
|
const fields = normalizeLines(req.body.fields);
|
||||||
|
const durations = normalizeLines(req.body.durations);
|
||||||
|
|
||||||
|
if (!levels.length || !fields.length || !durations.length) {
|
||||||
|
req.flash("error_msg", "Degree levels, fields and duration ranges cannot be empty.");
|
||||||
|
return res.redirect("/admin/programme/filters");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJsonFile("programme-filters", { levels, fields, durations });
|
||||||
|
req.flash("success_msg", "Programme filters updated successfully.");
|
||||||
|
res.redirect("/admin/programme/filters");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("programme.updateFilters:", err);
|
||||||
|
req.flash("error_msg", "Error updating programme filters.");
|
||||||
|
res.redirect("/admin/programme/filters");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/programmes/:id
|
* GET /api/programmes/:id
|
||||||
* Return a single programme matched by the slug `id` field.
|
* Return a single programme matched by the slug `id` field.
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"levels": [
|
||||||
|
"Bachelor's",
|
||||||
|
"Master's",
|
||||||
|
"truong's"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
"Computer Science",
|
||||||
|
"Data Analytics",
|
||||||
|
"Business & Admin",
|
||||||
|
"Healthcare",
|
||||||
|
"Finance & Accounting",
|
||||||
|
"Marketing"
|
||||||
|
],
|
||||||
|
"durations": [
|
||||||
|
"< 6 months",
|
||||||
|
"6-12 months",
|
||||||
|
"12-24 months",
|
||||||
|
"24-48 months",
|
||||||
|
"48+ months"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -34,7 +34,9 @@ const programmeSchema = new mongoose.Schema(
|
|||||||
title: { type: String, default: "" },
|
title: { type: String, default: "" },
|
||||||
description: { type: String, default: "" },
|
description: { type: String, default: "" },
|
||||||
duration: { type: String, default: "" },
|
duration: { type: String, default: "" },
|
||||||
|
durationInMonths: { type: Number, default: 0 },
|
||||||
cost: { type: String, default: "" },
|
cost: { type: String, default: "" },
|
||||||
|
fieldOfStudy: { type: String, default: "" },
|
||||||
selected: { type: Boolean, default: false },
|
selected: { type: Boolean, default: false },
|
||||||
link: { type: String, default: "" },
|
link: { type: String, default: "" },
|
||||||
shortName: { type: String, default: "" },
|
shortName: { type: String, default: "" },
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ router.get("/test-images", ensureAuthenticated, (req, res) => {
|
|||||||
|
|
||||||
// Programme Management
|
// Programme Management
|
||||||
router.get("/programme", ensureAuthenticated, programmeController.index);
|
router.get("/programme", ensureAuthenticated, programmeController.index);
|
||||||
|
router.get("/programme/filters", ensureAuthenticated, programmeController.filtersPage);
|
||||||
|
router.post("/programme/filters", ensureAuthenticated, programmeController.updateFilters);
|
||||||
router.get("/programme/edit/:id", ensureAuthenticated, programmeController.edit);
|
router.get("/programme/edit/:id", ensureAuthenticated, programmeController.edit);
|
||||||
router.post("/programme/update/:id", ensureAuthenticated, upload.single('heroImage'), programmeController.update);
|
router.post("/programme/update/:id", ensureAuthenticated, upload.single('heroImage'), programmeController.update);
|
||||||
router.post("/programme/delete/:id", ensureAuthenticated, programmeController.delete);
|
router.post("/programme/delete/:id", ensureAuthenticated, programmeController.delete);
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ router.get("/api/blog/:slug", blogController.apiShow);
|
|||||||
|
|
||||||
// Programmes API
|
// Programmes API
|
||||||
router.get("/api/programmes", programmeController.api);
|
router.get("/api/programmes", programmeController.api);
|
||||||
|
router.get("/api/programmes/filters", programmeController.apiFilters);
|
||||||
router.get("/api/programmes/:id", programmeController.apiDetail);
|
router.get("/api/programmes/:id", programmeController.apiDetail);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,12 +78,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-medium" for="fieldLevel">Level</label>
|
<label class="form-label fw-medium" for="fieldLevel">Level</label>
|
||||||
<input type="text" class="form-control" id="fieldLevel" name="level" value="<%= programme.level || '' %>" placeholder="Bachelor's Degree, Master's…">
|
<select class="form-select" id="fieldLevel" name="level">
|
||||||
|
<option value="">-- Select level --</option>
|
||||||
|
<% (levelOptions || []).forEach(function(level) { %>
|
||||||
|
<option value="<%= level %>" <%= programme.level === level ? 'selected' : '' %>><%= level %></option>
|
||||||
|
<% }) %>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-medium" for="fieldShortName">Short Name</label>
|
<label class="form-label fw-medium" for="fieldShortName">Short Name</label>
|
||||||
<input type="text" class="form-control" id="fieldShortName" name="shortName" value="<%= programme.shortName || '' %>" placeholder="MBA, B.S., Cert…">
|
<input type="text" class="form-control" id="fieldShortName" name="shortName" value="<%= programme.shortName || '' %>" placeholder="MBA, B.S., Cert…">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-medium" for="fieldFieldOfStudy">Field of Study</label>
|
||||||
|
<select class="form-select" id="fieldFieldOfStudy" name="fieldOfStudy">
|
||||||
|
<option value="">-- Select field --</option>
|
||||||
|
<option value="Computer Science" <%= programme.fieldOfStudy === 'Computer Science' ? 'selected' : '' %>>Computer Science</option>
|
||||||
|
<option value="Data Analytics" <%= programme.fieldOfStudy === 'Data Analytics' ? 'selected' : '' %>>Data Analytics</option>
|
||||||
|
<option value="Business & Admin" <%= programme.fieldOfStudy === 'Business & Admin' ? 'selected' : '' %>>Business & Admin</option>
|
||||||
|
<option value="Healthcare" <%= programme.fieldOfStudy === 'Healthcare' ? 'selected' : '' %>>Healthcare</option>
|
||||||
|
<option value="Finance & Accounting" <%= programme.fieldOfStudy === 'Finance & Accounting' ? 'selected' : '' %>>Finance & Accounting</option>
|
||||||
|
<option value="Marketing" <%= programme.fieldOfStudy === 'Marketing' ? 'selected' : '' %>>Marketing</option>
|
||||||
|
<option value="Education" <%= programme.fieldOfStudy === 'Education' ? 'selected' : '' %>>Education</option>
|
||||||
|
<option value="Engineering" <%= programme.fieldOfStudy === 'Engineering' ? 'selected' : '' %>>Engineering</option>
|
||||||
|
<option value="Psychology" <%= programme.fieldOfStudy === 'Psychology' ? 'selected' : '' %>>Psychology</option>
|
||||||
|
<option value="Cybersecurity" <%= programme.fieldOfStudy === 'Cybersecurity' ? 'selected' : '' %>>Cybersecurity</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<label class="form-label fw-medium" for="fieldTitle">Title <span class="text-danger">*</span></label>
|
<label class="form-label fw-medium" for="fieldTitle">Title <span class="text-danger">*</span></label>
|
||||||
<input type="text" class="form-control" id="fieldTitle" name="title" value="<%= programme.title || '' %>" required>
|
<input type="text" class="form-control" id="fieldTitle" name="title" value="<%= programme.title || '' %>" required>
|
||||||
@@ -93,8 +114,27 @@
|
|||||||
<textarea class="form-control" id="fieldDescription" name="description" rows="3"><%= programme.description || '' %></textarea>
|
<textarea class="form-control" id="fieldDescription" name="description" rows="3"><%= programme.description || '' %></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-medium" for="fieldDuration">Duration</label>
|
<label class="form-label fw-medium" for="fieldDuration">Duration (Display)</label>
|
||||||
<input type="text" class="form-control" id="fieldDuration" name="duration" value="<%= programme.duration || '' %>" placeholder="4 Years (Flexible)">
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="fieldDuration"
|
||||||
|
name="duration"
|
||||||
|
value="<%= programme.duration || '' %>"
|
||||||
|
placeholder="4 Years (Flexible)"
|
||||||
|
list="durationSuggestions"
|
||||||
|
>
|
||||||
|
<datalist id="durationSuggestions">
|
||||||
|
<% (durationOptions || []).forEach(function(duration) { %>
|
||||||
|
<option value="<%= duration %>"></option>
|
||||||
|
<% }) %>
|
||||||
|
</datalist>
|
||||||
|
<div class="form-text">Text hiển thị trên chương trình.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-medium" for="fieldDurationInMonths">Duration (months)</label>
|
||||||
|
<input type="number" class="form-control" id="fieldDurationInMonths" name="durationInMonths" value="<%= programme.durationInMonths || 0 %>" min="0" placeholder="48">
|
||||||
|
<div class="form-text">Dùng để match vào Duration filter.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label fw-medium" for="fieldFormat">Format</label>
|
<label class="form-label fw-medium" for="fieldFormat">Format</label>
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-4 mb-4 flex-wrap gap-2">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Programme Filters</h1>
|
||||||
|
<p class="text-muted mb-0">Manage centralized filter options used by frontend programme page.</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<% if (frontendUrl) { %>
|
||||||
|
<a href="<%= frontendUrl %>/programmes" class="btn btn-outline-primary" target="_blank" rel="noopener">
|
||||||
|
<i class="fas fa-external-link-alt me-2"></i>View Programmes Page
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
<a href="/admin/programme" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Back to programmes
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="/admin/programme/filters" method="POST" class="card shadow-sm border-0">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-medium" for="levelsInput">Degree level options</label>
|
||||||
|
<textarea
|
||||||
|
id="levelsInput"
|
||||||
|
name="levels"
|
||||||
|
class="form-control"
|
||||||
|
rows="14"
|
||||||
|
placeholder="One option per line"
|
||||||
|
><%= (filters.levels || []).join('\n') %></textarea>
|
||||||
|
<div class="form-text">Use one line per option. Example: Bachelor's, Master's.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-medium" for="fieldsInput">Field of Study options</label>
|
||||||
|
<textarea
|
||||||
|
id="fieldsInput"
|
||||||
|
name="fields"
|
||||||
|
class="form-control"
|
||||||
|
rows="14"
|
||||||
|
placeholder="One option per line"
|
||||||
|
><%= (filters.fields || []).join('\n') %></textarea>
|
||||||
|
<div class="form-text">Use one line per option. These appear as checkboxes in frontend filters.</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label fw-medium" for="durationsInput">Duration range options</label>
|
||||||
|
<textarea
|
||||||
|
id="durationsInput"
|
||||||
|
name="durations"
|
||||||
|
class="form-control"
|
||||||
|
rows="14"
|
||||||
|
placeholder="One option per line"
|
||||||
|
><%= (filters.durations || []).join('\n') %></textarea>
|
||||||
|
<div class="form-text">Use one line per option. Example: < 6 months, 12-24 months, 48+ months.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white border-top d-flex justify-content-end gap-2">
|
||||||
|
<a href="/admin/programme" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-2"></i>Save Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
<i class="fas fa-external-link-alt me-2"></i>View Programmes Page
|
<i class="fas fa-external-link-alt me-2"></i>View Programmes Page
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<a href="/admin/programme/filters" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-filter me-2"></i>Manage Filters
|
||||||
|
</a>
|
||||||
<a href="/admin/programme/edit/new" class="btn btn-primary shadow-sm">
|
<a href="/admin/programme/edit/new" class="btn btn-primary shadow-sm">
|
||||||
<i class="fas fa-plus me-2"></i>Add New Programme
|
<i class="fas fa-plus me-2"></i>Add New Programme
|
||||||
</a>
|
</a>
|
||||||
@@ -25,6 +28,8 @@
|
|||||||
<th class="ps-4" style="width:110px;">ID / Code</th>
|
<th class="ps-4" style="width:110px;">ID / Code</th>
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
<th>Level</th>
|
<th>Level</th>
|
||||||
|
<th>Field</th>
|
||||||
|
<th class="text-center" style="width:90px;">Months</th>
|
||||||
<th>Format</th>
|
<th>Format</th>
|
||||||
<th>Cost</th>
|
<th>Cost</th>
|
||||||
<th class="text-center" style="width:80px;">Courses</th>
|
<th class="text-center" style="width:80px;">Courses</th>
|
||||||
@@ -48,6 +53,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="badge bg-light text-dark border"><%= item.level || '—' %></span>
|
<span class="badge bg-light text-dark border"><%= item.level || '—' %></span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-muted small"><%= item.fieldOfStudy || '—' %></td>
|
||||||
|
<td class="text-center text-muted small"><%= item.durationInMonths || '—' %></td>
|
||||||
<td class="text-muted small"><%= item.format || '—' %></td>
|
<td class="text-muted small"><%= item.format || '—' %></td>
|
||||||
<td class="text-muted small"><%= item.monthlyCost || item.cost || '—' %></td>
|
<td class="text-muted small"><%= item.monthlyCost || item.cost || '—' %></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -75,7 +82,7 @@
|
|||||||
<% }); %>
|
<% }); %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center text-muted py-5">
|
<td colspan="10" class="text-center text-muted py-5">
|
||||||
<i class="fas fa-graduation-cap fa-2x mb-3 d-block opacity-25"></i>
|
<i class="fas fa-graduation-cap fa-2x mb-3 d-block opacity-25"></i>
|
||||||
No programmes found.
|
No programmes found.
|
||||||
<a href="/admin/programme/edit/new" class="d-block mt-2">Add your first programme →</a>
|
<a href="/admin/programme/edit/new" class="d-block mt-2">Add your first programme →</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user