diff --git a/controllers/headerController.js b/controllers/headerController.js index a237ffb..b20a18a 100644 --- a/controllers/headerController.js +++ b/controllers/headerController.js @@ -33,29 +33,23 @@ exports.index = async (req, res) => { // Prepare data for view const data = header ? { - topbar: { - contactInfo: { - phone: header.top?.phone || "", - email: header.top?.email || "", - location: header.top?.location || "", - }, - socialLinks: header.top?.socialLinks || [], - }, logo: header.logo?.light || "", + signInButton: header.signInButton || { + label: "Sign In", + href: "/signin", + }, + ctaButton: header.ctaButton || { + label: "Request Info", + href: "/request", + }, } : { - topbar: { - contactInfo: { - phone: "", - email: "", - location: "", - }, - socialLinks: [], - }, logo: "", + signInButton: { label: "Sign In", href: "/signin" }, + ctaButton: { label: "Request Info", href: "/request" }, }; - const activeTab = req.query.tab || "topbar"; + const activeTab = req.query.tab || "logo"; // Always fetch menu items to ensure they are available even if the user // switches tabs client-side @@ -123,14 +117,12 @@ exports.show = async (req, res) => { // Admin: Create header exports.store = async (req, res) => { try { - const { top, offcanvas, menu, logo, ctaButton, status, order } = req.body; + const { logo, signInButton, ctaButton, status, order } = req.body; const header = new Header({ - top, - offcanvas, - menu, - logo, - ctaButton, + logo: logo ? { light: logo } : {}, + signInButton: signInButton || { label: "Sign In", href: "/signin" }, + ctaButton: ctaButton || {}, status: status || "active", order: order || 1, }); @@ -152,129 +144,81 @@ exports.store = async (req, res) => { // Admin: Update header exports.update = async (req, res) => { try { - let { top, topbarJson, offcanvas, menu, logo, ctaButton, status, order } = - req.body; + const { logo, signInButton, ctaButton, status, order } = req.body; console.log("=== UPDATE REQUEST RECEIVED ==="); console.log("Raw body:", JSON.stringify(req.body, null, 2)); - console.log("topbarJson type:", typeof topbarJson); - console.log("topbarJson value:", topbarJson); - // Nếu có topbarJson, parse nó - if (topbarJson && typeof topbarJson === "string") { - try { - const parsedData = JSON.parse(topbarJson); - console.log("✓ Parsed topbarJson successfully:", parsedData); - // Chuyển đổi từ topbarData sang top format - top = { - phone: parsedData.contactInfo?.phone || "", - email: parsedData.contactInfo?.email || "", - location: parsedData.contactInfo?.location || "", - socialLinks: parsedData.socialLinks || [], - }; + // Upsert logic: find existing header or prepare to create new one + let header = await Header.findOne().sort({ order: 1 }); + let headerId = header?._id; - if (logo) { - updateData.logo = logoData; - } + // Capture BEFORE state for audit logging + const beforeData = header + ? JSON.parse(JSON.stringify(header.toObject())) + : {}; - console.log( - "Preparing to update header with data:", - JSON.stringify(updateData, null, 2), - ); + if (!header) { + console.log("No existing header found, creating new one"); + // Create new header document + header = new Header({ + logo: logo ? { light: logo } : {}, + signInButton: signInButton || { label: "Sign In", href: "/signin" }, + ctaButton: ctaButton || {}, + status: status || "active", + order: order || 1, + }); + await header.save(); + console.log("✓ Header created:", header._id); - const updatedHeader = await Header.findByIdAndUpdate( - headerId, - updateData, - { new: true, runValidators: true }, - ); - - if (!updatedHeader) { - console.error("✗ Header not found with ID:", headerId); - return res.status(404).json({ - success: false, - message: "Header not found", - }); - } - res.json({ - success: true, - message: "Header updated successfully", - data: updatedHeader, - }); - } catch (error) { - console.error("✗ Error updating header:", error); - res.status(400).json({ - success: false, - message: error.message, + // Audit log for creation + const afterData = JSON.parse(JSON.stringify(header.toObject())); + const changes = diffObject(beforeData, afterData); + if (changes.length > 0) { + await writeAuditLog({ + model: "Header", + documentId: header._id, + action: AUDIT_ACTIONS.UPDATE_HEADER, + before: beforeData, + after: afterData, + changes, + req, }); } + + return res.json({ + success: true, + message: "Header created successfully", + data: header, + }); } - // Nếu không có id, tìm header đầu tiên hoặc tạo mới - let headerId = req.params.id; - - if (!headerId) { - // Tìm header đầu tiên - let header = await Header.findOne().sort({ order: 1 }); - if (!header) { - console.log("No existing header found, creating new one"); - // Tạo header mới nếu chưa có - header = new Header({ - top, - offcanvas, - menu, - logo: logo ? { light: logo } : {}, - ctaButton, - status: status || "active", - order: order || 1, - }); - await header.save(); - console.log("✓ Header created:", header._id); - return res.json({ - success: true, - message: "Header created successfully", - data: header, - }); - } - headerId = header._id; - console.log("✓ Found existing header:", headerId); - } - - // Chuẩn bị dữ liệu logo - merge với dữ liệu cũ - let logoData = {}; + // Prepare logo data - merge with existing data + let logoData = header.logo || {}; if (logo) { - // Nếu có logo mới, lấy dữ liệu cũ và update light - const existingHeader = await Header.findById(headerId); logoData = { light: logo, - dark: existingHeader?.logo?.dark || "", - alt: existingHeader?.logo?.alt || "", + dark: header.logo?.dark || "", + alt: header.logo?.alt || "", }; } + // Prepare update data const updateData = { - top, - offcanvas, - menu, - ctaButton, - status, - order, + logo: logoData, + signInButton: signInButton || header.signInButton, + ctaButton: ctaButton || header.ctaButton, }; - if (logo) { - updateData.logo = logoData; - } + if (status !== undefined) updateData.status = status; + if (order !== undefined) updateData.order = order; console.log( "Preparing to update header with data:", JSON.stringify(updateData, null, 2), ); - // ✅ Capture BEFORE state - const beforeHeader = await Header.findById(headerId); - const beforeData = beforeHeader - ? JSON.parse(JSON.stringify(beforeHeader.toObject())) - : {}; - + // Update existing header const updatedHeader = await Header.findByIdAndUpdate(headerId, updateData, { new: true, runValidators: true, @@ -288,10 +232,10 @@ exports.update = async (req, res) => { }); } - // ✅ Capture AFTER state + // Capture AFTER state for audit logging const afterData = JSON.parse(JSON.stringify(updatedHeader.toObject())); - // ✅ AUDIT LOGGING - Header Updated + // Audit logging - Header Updated const changes = diffObject(beforeData, afterData); if (changes.length > 0) { await writeAuditLog({ @@ -384,23 +328,41 @@ exports.destroy = async (req, res) => { } }; -// Public API: Get active header +// Public API: Get header (returns header regardless of status) exports.api = async (req, res) => { try { - const header = await Header.findOne({ status: "active" }).sort({ + const header = await Header.findOne().sort({ order: 1, }); if (!header) { return res.status(404).json({ success: false, - message: "No active header found", + message: "No header found", }); } + const baseUrl = (process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`).replace(/\/$/, ''); + const rawLogoPath = header.logo?.light || ''; + const logoImage = rawLogoPath.startsWith('http') ? rawLogoPath : `${baseUrl}${rawLogoPath}`; + res.json({ success: true, - data: header, + data: { + logo: { + image: logoImage, + href: "/", + }, + signInButton: { + label: header.signInButton?.label || "Sign In", + href: header.signInButton?.href || "/signin", + }, + ctaButton: { + label: header.ctaButton?.label || "Request Info", + href: header.ctaButton?.href || "/request", + }, + status: header.status || "active", + }, }); } catch (error) { res.status(500).json({ @@ -410,28 +372,3 @@ exports.api = async (req, res) => { } }; -// Public API: Get menu tree structure -exports.getMenuTreeAPI = async (req, res) => { - try { - const header = await Header.findOne({ status: "active" }).sort({ - order: 1, - }); - - if (!header || !header.menu) { - return res.status(404).json({ - success: false, - message: "No active menu found", - }); - } - - res.json({ - success: true, - data: header.menu, - }); - } catch (error) { - res.status(500).json({ - success: false, - message: error.message, - }); - } -}; diff --git a/controllers/headerMenuController.js b/controllers/headerMenuController.js index 0c1ddfb..a43571f 100644 --- a/controllers/headerMenuController.js +++ b/controllers/headerMenuController.js @@ -21,6 +21,7 @@ const buildMenuTree = (items, parentId = null, isPublic = false) => { title: item.title, url: item.url, type: item.type, + status: item.status || 'active', }; } @@ -196,7 +197,7 @@ exports.reorder = async (req, res) => { // Public API: Get active menu as clean tree exports.api = async (req, res) => { try { - const items = await HeaderMenu.find({ status: "active" }).sort({ order: 1 }); + const items = await HeaderMenu.find().sort({ order: 1 }); const tree = buildMenuTree(items, null, true); res.json({ success: true, data: tree }); } catch (error) { diff --git a/models/header.js b/models/header.js index 2c03f58..0508f03 100644 --- a/models/header.js +++ b/models/header.js @@ -1,84 +1,7 @@ const mongoose = require("mongoose"); -const socialLinkSchema = new mongoose.Schema( - { - platform: { - type: String, - required: true, - enum: ["linkedin", "twitter", "instagram", "youtube", "facebook"], - }, - url: { - type: String, - required: true, - }, - icon: String, - order: { - type: Number, - default: 0, - }, - }, - { _id: false }, -); - -const languageSchema = new mongoose.Schema( - { - name: { - type: String, - required: true, - }, - value: { - type: String, - required: true, - }, - }, - { _id: false }, -); - -const menuItemSchema = new mongoose.Schema( - { - label: { - type: String, - required: true, - }, - href: { - type: String, - required: true, - }, - icon: String, - order: { - type: Number, - default: 0, - }, - children: [this], - }, - { _id: false }, -); - const headerSchema = new mongoose.Schema( { - // Top bar - top: { - phone: String, - email: String, - location: String, - socialLinks: [socialLinkSchema], - languages: [languageSchema], - }, - - // Offcanvas - offcanvas: { - description: String, - contactInfo: { - address: String, - email: String, - workingHours: String, - phone: String, - }, - }, - - // Menu - menu: [menuItemSchema], - // Logo logo: { light: String, @@ -86,6 +9,18 @@ const headerSchema = new mongoose.Schema( alt: String, }, + // Sign In Button + signInButton: { + label: { + type: String, + default: "Sign In", + }, + href: { + type: String, + default: "/signin", + }, + }, + // CTA Button ctaButton: { label: String, diff --git a/public/uploads/header/logo.jpg b/public/uploads/header/logo.jpg new file mode 100644 index 0000000..23babee Binary files /dev/null and b/public/uploads/header/logo.jpg differ diff --git a/routes/index.js b/routes/index.js index 529305e..589c3f1 100644 --- a/routes/index.js +++ b/routes/index.js @@ -50,9 +50,6 @@ router.get("/api/about-us", aboutUsController.getAbout); // Header API route router.get("/api/header", headerController.api); -// Menu Tree API route (for frontend) -router.get("/api/menu-tree", headerController.getMenuTreeAPI); - // Header Menu New Module API router.get("/api/header-menu", headerMenuController.api); diff --git a/scripts/seed_header_from_json.js b/scripts/seed_header_from_json.js new file mode 100644 index 0000000..ad902b0 --- /dev/null +++ b/scripts/seed_header_from_json.js @@ -0,0 +1,190 @@ +require('dotenv').config(); +const mongoose = require('mongoose'); +const slugify = require('slugify'); + +// Data from lams/app/components/layout/Header/header.json +const headerJsonData = { + "logo": { + "image": "/uploads/header/logo.jp", + "href": "/" + }, + "navLinks": [ + { + "label": "About Us", + "href": "/about", + "children": [ + { + "label": "History & Milestones", + "href": "/about/history" + }, + { + "label": "Accreditation", + "href": "/about/accreditation" + }, + { + "label": "Partnerships", + "href": "/about/partnerships" + } + ] + }, + { + "label": "Programs", + "href": "/programmes" + }, + { + "label": "Student Support", + "href": "/student-support" + }, + { + "label": "Admissions & Tuition", + "href": "/admissions" + }, + { + "label": "Blog", + "href": "/blog" + }, + { + "label": "Contact", + "href": "/contact" + } + ], + "actions": { + "signIn": { + "label": "Sign In", + "href": "/signin" + }, + "cta": { + "label": "Request Info", + "href": "/request" + } + } +}; + +async function seedHeader() { + try { + console.log('=== Seed Header Data from JSON ==='); + console.log('Connecting to MongoDB...'); + + await mongoose.connect(process.env.MONGODB_URI); + console.log('✓ Connected to MongoDB'); + + const Header = require('../models/header'); + const HeaderMenu = require('../models/headerMenu'); + + // Step 1: Seed Header document + console.log('\nStep 1: Seeding Header document...'); + + const existingHeader = await Header.findOne(); + + if (existingHeader) { + console.log('⚠ Header document already exists. Updating...'); + existingHeader.logo = { + light: headerJsonData.logo.image, + dark: '', + alt: 'LAMS Logo', + }; + existingHeader.signInButton = { + label: headerJsonData.actions.signIn.label, + href: headerJsonData.actions.signIn.href, + }; + existingHeader.ctaButton = { + label: headerJsonData.actions.cta.label, + href: headerJsonData.actions.cta.href, + style: 'primary', + }; + existingHeader.status = 'active'; + await existingHeader.save(); + console.log('✓ Header document updated'); + } else { + const header = new Header({ + logo: { + light: headerJsonData.logo.image, + dark: '', + alt: 'LAMS Logo', + }, + signInButton: { + label: headerJsonData.actions.signIn.label, + href: headerJsonData.actions.signIn.href, + }, + ctaButton: { + label: headerJsonData.actions.cta.label, + href: headerJsonData.actions.cta.href, + style: 'primary', + }, + status: 'active', + order: 1, + }); + await header.save(); + console.log('✓ Header document created'); + } + + // Step 2: Seed HeaderMenu documents + console.log('\nStep 2: Seeding HeaderMenu documents...'); + + const existingMenuCount = await HeaderMenu.countDocuments(); + + if (existingMenuCount > 0) { + console.log(`⚠ Found ${existingMenuCount} existing menu items. Skipping menu seed.`); + console.log(' To re-seed menu, delete existing menu items first.'); + } else { + let order = 0; + + for (const navLink of headerJsonData.navLinks) { + // Create parent menu item + const parentSlug = slugify(navLink.label, { lower: true, strict: true }); + const parentMenu = new HeaderMenu({ + title: navLink.label, + slug: parentSlug, + url: navLink.href, + parentId: null, + order: order++, + status: 'active', + type: 'internal', + }); + await parentMenu.save(); + console.log(` ✓ Created parent menu: ${navLink.label}`); + + // Create children menu items if they exist + if (navLink.children && navLink.children.length > 0) { + let childOrder = 0; + for (const child of navLink.children) { + const childSlug = slugify(child.label, { lower: true, strict: true }); + const childMenu = new HeaderMenu({ + title: child.label, + slug: childSlug, + url: child.href, + parentId: parentMenu._id, + order: childOrder++, + status: 'active', + type: 'internal', + }); + await childMenu.save(); + console.log(` ✓ Created child menu: ${child.label}`); + } + } + } + + console.log(`✓ Created ${order} parent menu items with their children`); + } + + console.log('\n=== Seed Complete ==='); + await mongoose.disconnect(); + console.log('✓ Disconnected from MongoDB'); + + } catch (error) { + console.error('✗ Seed failed:', error); + process.exit(1); + } +} + +// Run seed if this script is executed directly +if (require.main === module) { + seedHeader() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); +} + +module.exports = seedHeader; diff --git a/views/admin/header/index.ejs b/views/admin/header/index.ejs index 03cdb93..e3dfc43 100644 --- a/views/admin/header/index.ejs +++ b/views/admin/header/index.ejs @@ -12,25 +12,13 @@
- - - +