forked from UKSOURCE/cms.hailearning.edu.vn
934 lines
33 KiB
Plaintext
934 lines
33 KiB
Plaintext
<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> |