forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
934
views/admin/header/index.ejs
Normal file
934
views/admin/header/index.ejs
Normal file
@@ -0,0 +1,934 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Header Management</h1>
|
||||
<p class="text-muted mb-0">Edit header content and menu structure</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/header/update" method="POST" class="content-with-fixed-buttons" id="headerForm">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="topbarJson" id="topbarJson">
|
||||
<input type="hidden" name="logo" id="logoInput">
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar">
|
||||
<input type="hidden" name="menuUpdates" id="menuUpdates">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#topbar" role="tab">
|
||||
<i class="fas fa-bars me-2"></i>Topbar
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#logo" role="tab">
|
||||
<i class="fas fa-image me-2"></i>Logo
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#menu" role="tab">
|
||||
<i class="fas fa-sitemap me-2"></i>Menu Structure
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Topbar Tab -->
|
||||
<div class="tab-pane fade show active" id="topbar" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Topbar Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Contact Info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Contact Information</h6>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Phone Number</label>
|
||||
<input type="text" class="form-control" id="contactPhone"
|
||||
value="<%= data.topbar.contactInfo.phone %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Email</label>
|
||||
<input type="email" class="form-control" id="contactEmail"
|
||||
value="<%= data.topbar.contactInfo.email || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Quick Links</h6>
|
||||
<div id="quickLinksContainer">
|
||||
<% data.topbar.links.forEach((link, index)=> { %>
|
||||
<div class="card mb-3 border">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Link Text</label>
|
||||
<input type="text" class="form-control quick-link-text" value="<%= link.text %>"
|
||||
data-index="<%= index %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">URL</label>
|
||||
<input type="url" class="form-control quick-link-url" value="<%= link.url %>"
|
||||
data-index="<%= index %>">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-quick-link"
|
||||
data-index="<%= index %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="addQuickLink">
|
||||
<i class="fas fa-plus me-1"></i>Add Quick Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo Tab -->
|
||||
<div class="tab-pane fade" id="logo" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Logo Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Logo Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="logoImage" value="<%= data.logo %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="logoImage" data-image-type="layout">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 200x60px</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<% if (data.logo) { %>
|
||||
<img src="<%= data.logo %>" class="img-thumbnail"
|
||||
style="max-height: 100px; max-width: 300px; object-fit: contain; background: #b8b76a;"
|
||||
alt="Logo preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Structure Tab -->
|
||||
<div class="tab-pane fade" id="menu" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Menu Structure</h6>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-success btn-sm me-2" id="saveMenuChanges"
|
||||
style="display: none;">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="refreshMenuTree">
|
||||
<i class="fas fa-sync-alt me-1"></i>Refresh Menu Tree
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Click "Save Changes" on top right to apply changes.
|
||||
</div>
|
||||
|
||||
<div id="menuTreeContainer">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading menu structure...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Tree Template -->
|
||||
<template id="menuTreeTemplate">
|
||||
<div class="menu-tree">
|
||||
<div class="menu-items">
|
||||
<!-- Menu items will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Menu Item Template -->
|
||||
<template id="menuItemTemplate">
|
||||
<div class="menu-item card mb-2 border" data-menuid="">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="badge bg-primary me-2 menu-type"></span>
|
||||
<h6 class="mb-0 menu-title"></h6>
|
||||
<small class="text-muted ms-2 menu-url"></small>
|
||||
</div>
|
||||
<div class="menu-details">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Title:</label>
|
||||
<input type="text" class="form-control form-control-sm menu-title-input" style="width: 120px;">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1">Order:</label>
|
||||
<input type="number" class="form-control form-control-sm menu-order-input" min="0" style="width: 80px;">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1">Type:</label>
|
||||
<select class="form-select form-select-sm menu-type-select" style="width: 100px;">
|
||||
<option value="static">Static</option>
|
||||
<option value="page">Page</option>
|
||||
<option value="level">Level</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Menu Parent:</label>
|
||||
<select class="form-select form-select-sm menu-parent-select" style="width: 130px;">
|
||||
<option value="">Main menu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-check form-switch fetch-toggle" style="display: none;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" data-menuid="">
|
||||
<label class="form-check-label form-label-sm">Programmes</label>
|
||||
</div>
|
||||
<div class="form-check form-switch active-toggle">
|
||||
<input class="form-check-input" type="checkbox" role="switch" data-menuid="" checked>
|
||||
<label class="form-check-label form-label-sm">Active</label>
|
||||
</div>
|
||||
<small class="text-muted menu-fetch-display"></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-2">
|
||||
<button type="button" class="btn btn-outline-info btn-sm view-programmes" style="display: none;">
|
||||
<i class="fas fa-list me-1"></i>Programmes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-children mt-2" style="display: none;">
|
||||
<!-- Children will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.menu-tree {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-children {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid #e9ecef;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.menu-type {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.content-with-fixed-buttons {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-label-sm {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.menu-item .form-control-sm,
|
||||
.menu-item .form-select-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.menu-item .card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.menu-item .row.g-2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item .col-md-3,
|
||||
.menu-item .col-md-6 {
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
/* Toggle switch styling */
|
||||
.form-check-input:checked {
|
||||
background-color: #b8b76a;
|
||||
border-color: #b8b76a;
|
||||
}
|
||||
|
||||
.form-check-input:focus {
|
||||
border-color: #b8b76a;
|
||||
box-shadow: 0 0 0 0.25rem rgba(4, 78, 39, 0.25);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fetch-toggle {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let quickLinkIndex = <%= data.topbar.links.length %>;
|
||||
|
||||
// Initialize form data
|
||||
updateHiddenInputs();
|
||||
|
||||
// Handle Active Tab Persistence
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTabObj = urlParams.get('activeTab');
|
||||
|
||||
if (activeTabObj) {
|
||||
const tabTrigger = document.querySelector(`a[href="#${activeTabObj}"]`);
|
||||
if (tabTrigger) {
|
||||
new bootstrap.Tab(tabTrigger).show();
|
||||
// Update hidden input to match restored tab
|
||||
document.getElementById('activeTabInput').value = activeTabObj;
|
||||
}
|
||||
}
|
||||
|
||||
// Update hidden input on tab change
|
||||
document.querySelectorAll('a[data-bs-toggle="tab"]').forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', function (event) {
|
||||
const targetId = event.target.getAttribute('href').substring(1); // remove #
|
||||
document.getElementById('activeTabInput').value = targetId;
|
||||
});
|
||||
});
|
||||
|
||||
// Load menu tree on tab show
|
||||
document.querySelector('a[href="#menu"]').addEventListener('shown.bs.tab', function () {
|
||||
loadMenuTree();
|
||||
});
|
||||
|
||||
// Add Quick Link
|
||||
document.getElementById('addQuickLink').addEventListener('click', function () {
|
||||
const container = document.getElementById('quickLinksContainer');
|
||||
const newLink = document.createElement('div');
|
||||
newLink.className = 'card mb-3 border';
|
||||
newLink.innerHTML =
|
||||
'<div class="card-body">' +
|
||||
'<div class="row g-3">' +
|
||||
'<div class="col-md-5">' +
|
||||
'<label class="form-label fw-medium">Link Text</label>' +
|
||||
'<input type="text" class="form-control quick-link-text" value="" data-index="' + quickLinkIndex + '">' +
|
||||
'</div>' +
|
||||
'<div class="col-md-6">' +
|
||||
'<label class="form-label fw-medium">URL</label>' +
|
||||
'<input type="url" class="form-control quick-link-url" value="" data-index="' + quickLinkIndex + '">' +
|
||||
'</div>' +
|
||||
'<div class="col-md-1">' +
|
||||
'<label class="form-label"> </label>' +
|
||||
'<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-quick-link" data-index="' + quickLinkIndex + '">' +
|
||||
'<i class="fas fa-trash"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
container.appendChild(newLink);
|
||||
quickLinkIndex++;
|
||||
updateHiddenInputs();
|
||||
});
|
||||
|
||||
// Remove Quick Link
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.remove-quick-link')) {
|
||||
e.target.closest('.card').remove();
|
||||
updateHiddenInputs();
|
||||
}
|
||||
});
|
||||
|
||||
// Update hidden inputs when form changes
|
||||
document.addEventListener('input', updateHiddenInputs);
|
||||
document.addEventListener('change', updateHiddenInputs);
|
||||
|
||||
// Refresh Menu Tree
|
||||
document.getElementById('refreshMenuTree').addEventListener('click', function () {
|
||||
loadMenuTree();
|
||||
});
|
||||
|
||||
// Save Menu Changes
|
||||
document.getElementById('saveMenuChanges').addEventListener('click', function () {
|
||||
saveMenuChanges();
|
||||
});
|
||||
|
||||
// Initialize image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('headerForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
updateHiddenInputs();
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Submit form
|
||||
this.submit();
|
||||
});
|
||||
|
||||
function updateHiddenInputs() {
|
||||
// Update topbar JSON
|
||||
const topbarData = {
|
||||
contactInfo: {
|
||||
phone: document.getElementById('contactPhone').value,
|
||||
email: document.getElementById('contactEmail').value
|
||||
},
|
||||
links: []
|
||||
};
|
||||
|
||||
// Collect quick links
|
||||
document.querySelectorAll('.quick-link-text').forEach((input, index) => {
|
||||
const urlInput = document.querySelector(`.quick-link-url[data-index="${input.dataset.index}"]`);
|
||||
if (input.value.trim() && urlInput.value.trim()) {
|
||||
topbarData.links.push({
|
||||
text: input.value.trim(),
|
||||
url: urlInput.value.trim()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('topbarJson').value = JSON.stringify(topbarData);
|
||||
document.getElementById('logoInput').value = document.getElementById('logoImage').value;
|
||||
|
||||
// Update menu updates hidden input
|
||||
try {
|
||||
const menuUpdates = collectMenuUpdates();
|
||||
document.getElementById('menuUpdates').value = JSON.stringify(menuUpdates);
|
||||
} catch (e) {
|
||||
console.error('Error collecting menu updates:', e);
|
||||
// Fallback to empty array to avoid server errors
|
||||
document.getElementById('menuUpdates').value = '[]';
|
||||
}
|
||||
}
|
||||
|
||||
function loadMenuTree() {
|
||||
const container = document.getElementById('menuTreeContainer');
|
||||
container.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading menu structure...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch('/admin/header/menu-tree')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Store all menu items for parent options
|
||||
allMenuItems = flattenMenuItems(data);
|
||||
renderMenuTree(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading menu tree:', error);
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Error loading menu structure. Please try again.
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function flattenMenuItems(menuItems) {
|
||||
const flattened = [];
|
||||
|
||||
function flatten(items) {
|
||||
items.forEach(item => {
|
||||
flattened.push({
|
||||
menuid: item.menuid,
|
||||
title: item.title,
|
||||
parent: item.parent
|
||||
});
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
flatten(item.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
flatten(menuItems);
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function renderMenuTree(menuItems) {
|
||||
const container = document.getElementById('menuTreeContainer');
|
||||
const template = document.getElementById('menuTreeTemplate');
|
||||
const menuTree = template.content.cloneNode(true);
|
||||
const menuItemsContainer = menuTree.querySelector('.menu-items');
|
||||
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = createMenuItem(item);
|
||||
menuItemsContainer.appendChild(menuItem);
|
||||
});
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(menuTree);
|
||||
}
|
||||
|
||||
function createMenuItem(item) {
|
||||
const template = document.getElementById('menuItemTemplate');
|
||||
const menuItem = template.content.cloneNode(true);
|
||||
|
||||
const card = menuItem.querySelector('.menu-item');
|
||||
const typeBadge = card.querySelector('.menu-type');
|
||||
const title = card.querySelector('.menu-title');
|
||||
const url = card.querySelector('.menu-url');
|
||||
const titleInput = card.querySelector('.menu-title-input');
|
||||
const orderInput = card.querySelector('.menu-order-input');
|
||||
const typeSelect = card.querySelector('.menu-type-select');
|
||||
const parentSelect = card.querySelector('.menu-parent-select');
|
||||
const fetchToggle = card.querySelector('.fetch-toggle');
|
||||
const fetchCheckbox = card.querySelector('.fetch-toggle input');
|
||||
const activeToggle = card.querySelector('.active-toggle');
|
||||
const activeCheckbox = card.querySelector('.active-toggle input');
|
||||
const fetchDisplay = card.querySelector('.menu-fetch-display');
|
||||
const viewProgrammesBtn = card.querySelector('.view-programmes');
|
||||
const childrenContainer = card.querySelector('.menu-children');
|
||||
|
||||
// Set content
|
||||
card.dataset.menuid = item.menuid;
|
||||
typeBadge.textContent = item.type;
|
||||
title.textContent = item.title;
|
||||
url.textContent = item.url;
|
||||
titleInput.value = item.title;
|
||||
orderInput.value = item.order;
|
||||
typeSelect.value = item.type;
|
||||
|
||||
// Populate parent select options
|
||||
populateParentOptions(parentSelect, item.menuid);
|
||||
parentSelect.value = item.parent || '';
|
||||
|
||||
// Show fetch toggle for level type menus
|
||||
if (item.type === 'level') {
|
||||
fetchToggle.style.display = 'block';
|
||||
fetchCheckbox.checked = item.fetch;
|
||||
fetchCheckbox.dataset.menuid = item.menuid;
|
||||
|
||||
// Add change listener for fetch toggle
|
||||
fetchCheckbox.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
}
|
||||
|
||||
// Set active toggle state
|
||||
activeCheckbox.checked = item.isActive !== false;
|
||||
activeCheckbox.dataset.menuid = item.menuid;
|
||||
|
||||
// Add change listener for active toggle
|
||||
activeCheckbox.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
|
||||
// Add change listeners
|
||||
titleInput.addEventListener('input', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
orderInput.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
typeSelect.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
// Show/hide fetch toggle based on type
|
||||
if (this.value === 'level') {
|
||||
fetchToggle.style.display = 'block';
|
||||
} else {
|
||||
fetchToggle.style.display = 'none';
|
||||
fetchCheckbox.checked = false;
|
||||
}
|
||||
});
|
||||
parentSelect.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
|
||||
// Show programmes button for level types with fetch=true
|
||||
if (item.type === 'level' && item.fetch) {
|
||||
viewProgrammesBtn.style.display = 'inline-block';
|
||||
viewProgrammesBtn.addEventListener('click', function () {
|
||||
loadProgrammes(item.menuid, childrenContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// Add children if any
|
||||
if (item.children && item.children.length > 0) {
|
||||
childrenContainer.style.display = 'block';
|
||||
item.children.forEach(child => {
|
||||
const childItem = createMenuItem(child);
|
||||
childrenContainer.appendChild(childItem);
|
||||
});
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
function loadProgrammes(menuId, container) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<small class="text-muted">Loading programmes...</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/admin/header/programmes/${menuId}`)
|
||||
.then(response => response.json())
|
||||
.then(programmes => {
|
||||
renderProgrammes(programmes, container);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading programmes:', error);
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-danger alert-sm">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
Error loading programmes
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderProgrammes(programmes, container) {
|
||||
if (programmes.length === 0) {
|
||||
container.innerHTML = '<small class="text-muted">No programmes found</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
const programmesList = document.createElement('div');
|
||||
programmesList.className = 'list-group list-group-flush';
|
||||
|
||||
programmes.forEach(programme => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item d-flex justify-content-between align-items-center py-2';
|
||||
item.innerHTML = `
|
||||
<div>
|
||||
<strong>${programme.name}</strong>
|
||||
<br>
|
||||
<small class="text-muted">${programme.code}</small>
|
||||
</div>
|
||||
<a href="${programme.url}" class="btn btn-outline-primary btn-sm" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
`;
|
||||
programmesList.appendChild(item);
|
||||
});
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(programmesList);
|
||||
}
|
||||
|
||||
// Global variable to store all menu items for parent options
|
||||
let allMenuItems = [];
|
||||
|
||||
function populateParentOptions(select, currentMenuId) {
|
||||
// Clear existing options except the first one
|
||||
while (select.children.length > 1) {
|
||||
select.removeChild(select.lastChild);
|
||||
}
|
||||
|
||||
// Add options for all menu items except the current one
|
||||
allMenuItems.forEach(item => {
|
||||
if (item.menuid !== currentMenuId) {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.menuid;
|
||||
option.textContent = item.title;
|
||||
select.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showSaveButton() {
|
||||
document.getElementById('saveMenuChanges').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function collectMenuUpdates() {
|
||||
const menuItems = document.querySelectorAll('.menu-item');
|
||||
const updates = [];
|
||||
|
||||
menuItems.forEach(item => {
|
||||
const menuId = item.dataset.menuid;
|
||||
const titleInput = item.querySelector('.menu-title-input');
|
||||
const orderInput = item.querySelector('.menu-order-input');
|
||||
const typeSelect = item.querySelector('.menu-type-select');
|
||||
const parentSelect = item.querySelector('.menu-parent-select');
|
||||
const fetchCheckbox = item.querySelector('.fetch-toggle input');
|
||||
const activeCheckbox = item.querySelector('.active-toggle input');
|
||||
|
||||
const updateData = {
|
||||
menuid: menuId,
|
||||
title: titleInput.value.trim(),
|
||||
order: parseInt(orderInput.value) || 0,
|
||||
type: typeSelect.value,
|
||||
parent: parentSelect.value || null
|
||||
};
|
||||
|
||||
// Add fetch status for level type menus
|
||||
if (fetchCheckbox) {
|
||||
updateData.fetch = fetchCheckbox.checked;
|
||||
}
|
||||
|
||||
// Add isActive status
|
||||
if (activeCheckbox) {
|
||||
updateData.isActive = activeCheckbox.checked;
|
||||
}
|
||||
|
||||
updates.push(updateData);
|
||||
});
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
function saveMenuChanges() {
|
||||
const updates = collectMenuUpdates();
|
||||
|
||||
// Show loading state
|
||||
const saveBtn = document.getElementById('saveMenuChanges');
|
||||
const originalText = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
// Send update request
|
||||
fetch('/admin/header/update-menu', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ updates })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
// Hide save button
|
||||
saveBtn.style.display = 'none';
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalText;
|
||||
|
||||
// Show success message
|
||||
showAlert('Menu structure updated successfully!', 'success');
|
||||
|
||||
// Reload menu tree after a short delay
|
||||
setTimeout(() => {
|
||||
loadMenuTree();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to update menu');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving menu changes:', error);
|
||||
showAlert('Error updating menu: ' + error.message, 'danger');
|
||||
|
||||
// Restore button
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('menuTreeContainer');
|
||||
container.parentElement.insertBefore(alertDiv, container);
|
||||
|
||||
// Auto remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentElement) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Tạo input file ẩn
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
// Xử lý khi chọn file
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
// Tạo FormData
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
// Disable nút upload và hiển thị loading
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn.innerHTML;
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
|
||||
// Gửi request upload
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
// Cập nhật đường dẫn ảnh vào input
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) {
|
||||
throw new Error('Target input not found');
|
||||
}
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Tìm hoặc tạo preview container
|
||||
let imgPreview = input.parentElement.nextElementSibling;
|
||||
while (imgPreview && !imgPreview.classList.contains('mt-2')) {
|
||||
imgPreview = imgPreview.nextElementSibling;
|
||||
}
|
||||
|
||||
if (!imgPreview) {
|
||||
// Tạo mới phần tử preview nếu chưa có
|
||||
imgPreview = document.createElement('div');
|
||||
imgPreview.className = 'mt-2';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
|
||||
// Set style dựa vào loại ảnh
|
||||
if (targetInput.toLowerCase().includes('logo') || targetInput.toLowerCase().includes('icon')) {
|
||||
img.style.maxHeight = '100px';
|
||||
img.style.maxWidth = '300px';
|
||||
img.style.objectFit = 'contain';
|
||||
} else {
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
}
|
||||
|
||||
img.alt = 'Image preview';
|
||||
imgPreview.appendChild(img);
|
||||
input.parentElement.parentElement.appendChild(imgPreview);
|
||||
}
|
||||
|
||||
// Cập nhật ảnh preview
|
||||
const img = imgPreview.querySelector('img');
|
||||
if (img) {
|
||||
img.src = result.path;
|
||||
}
|
||||
|
||||
// Restore nút upload
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(fileInput);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Upload failed: ' + error.message);
|
||||
|
||||
// Restore nút upload
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
|
||||
// Cleanup
|
||||
if (document.body.contains(fileInput)) {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger file selection
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user