Files
cms.uldp.edu.vn/views/admin/level/index.ejs
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

744 lines
32 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);">
<% const formattedType = currentType.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); %>
<%= formattedType %> Level
</h1>
</div>
<div class="d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="levelTypeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<%= formattedType %>
</button>
<ul class="dropdown-menu" aria-labelledby="levelTypeDropdown">
<% levelTypes.forEach(type => { %>
<li>
<a class="dropdown-item <%= type === currentType ? 'active' : '' %>" href="/admin/level?type=<%= type %>">
<%= type.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') %>
</a>
</li>
<% }); %>
</ul>
</div>
<!-- Thêm nút tạo mới type -->
<a href="/admin/level/create" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Add New Level Type
</a>
<!-- Nút xóa type hiện tại (chỉ hiển thị với non-default types) -->
<% if (!['undergraduate', 'postgraduate', 'pre-university'].includes(currentType)) { %>
<button type="button" class="btn btn-outline-danger" data-custom-modal="open" data-type="<%= currentType %>">
<i class="fas fa-trash me-1"></i>Delete This Level Type
</button>
<% } %>
</div>
</div>
<div class="row">
<div class="col-12">
<form id="levelForm" action="/admin/level/update?type=<%= currentType %>" method="POST" class="content-with-fixed-buttons">
<input type="hidden" name="type" value="<%= currentType %>">
<input type="hidden" name="banner" id="bannerJson">
<input type="hidden" name="overview" id="overviewJson">
<input type="hidden" name="requirements" id="requirementsJson">
<input type="hidden" name="action_buttons" id="actionbuttonsJson">
<input type="hidden" name="why_study" id="whystudyJson">
<!-- Level Type and Brochure Field -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Level Type</label>
<input type="text" class="form-control" value="<%= currentType.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') %>" readonly>
<div class="form-text text-muted">This is the level type identifier. It cannot be changed.</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Brochure Link</label>
<input type="text" class="form-control" id="levelBrochure" name="brochure" value="<%= data.brochure || '#' %>" placeholder="Enter brochure URL or # for no link">
<div class="form-text text-muted">Link to the level brochure. Use # if no brochure is available.</div>
</div>
</div>
</div>
</div>
<!-- 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="#banner" role="tab">
<i class="fas fa-image me-2"></i>Banner
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#overview" role="tab">
<i class="fas fa-info-circle me-2"></i>Overview
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#requirements" role="tab">
<i class="fas fa-list-check me-2"></i>Requirements
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#actions" role="tab">
<i class="fas fa-link me-2"></i>Action Buttons
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#why" role="tab">
<i class="fas fa-question-circle me-2"></i>Why Study
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Banner Tab -->
<div class="tab-pane fade show active" id="banner" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Banner Image</label>
<div class="row">
<div class="col-md-6 d-flex align-items-baseline">
<div class="input-group">
<input type="text" class="form-control" id="bannerImage" value="<%= data.banner.image %>">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="bannerImage">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6">
<!-- Ảnh preview cho banner -->
<div class="image-preview mt-2">
<% if (data.banner.image) { %>
<img src="<%= data.banner.image %>" alt="Banner preview" class="img-thumbnail" style="height:200px;width:100%;object-fit:cover;">
<% } %>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="bannerTitle" value="<%= data.banner.title %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Text</label>
<textarea class="form-control" id="bannerText" rows="3"><%= data.banner.text %></textarea>
</div>
</div>
</div>
<!-- Overview Tab -->
<div class="tab-pane fade" id="overview" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="overviewTitle" value="<%= data.overview.title %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Paragraphs</label>
<div id="overviewParagraphs">
<% (data.overview.paragraphs || []).forEach((paragraph, index) => { %>
<div class="mb-3 d-flex gap-2">
<textarea class="form-control" rows="3"><%= paragraph %></textarea>
<button type="button" class="btn btn-outline-danger" onclick="removeParagraph(this)">
<i class="fas fa-times"></i>
</button>
</div>
<% }); %>
</div>
<button type="button" class="btn btn-outline-primary mt-2" onclick="addParagraph('overviewParagraphs')">
<i class="fas fa-plus me-1"></i>Add Paragraph
</button>
</div>
<!-- Contact Info -->
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Contact Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" class="form-control" id="contactTitle" value="<%= data.overview.contact_info.title %>">
</div>
<div class="mb-3">
<label class="form-label">Subtitle</label>
<input type="text" class="form-control" id="contactSubtitle" value="<%= data.overview.contact_info.subtitle %>">
</div>
<div id="contactItems">
<% (data.overview.contact_info.items || []).forEach((item, index) => { %>
<div class="mb-3">
<input type="text" class="form-control" value="<%= item.text %>">
</div>
<% }); %>
</div>
</div>
</div>
</div>
<!-- Social Info -->
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Social Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" class="form-control" id="socialTitle" value="<%= data.overview.social_info.title %>">
</div>
<div id="socialLinks">
<% (data.overview.social_info.social_links || []).forEach((link, index) => { %>
<div class="mb-3 border p-3">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Image</label>
<div class="input-group">
<input type="text" class="form-control" value="<%= link.image %>">
<button type="button" class="btn btn-outline-primary btn-upload-image">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
<div class="col-md-4">
<label class="form-label">URL</label>
<input type="text" class="form-control" value="<%= link.url %>">
</div>
<div class="col-md-4">
<label class="form-label">Alt Text</label>
<input type="text" class="form-control" value="<%= link.alt %>">
</div>
</div>
</div>
<% }); %>
</div>
<div class="mt-4">
<h6>Apply Button</h6>
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Text</label>
<input type="text" class="form-control" id="applyButtonText" value="<%= data.overview.social_info.apply_button.text %>">
</div>
<div class="col-md-6">
<label class="form-label">URL</label>
<input type="text" class="form-control" id="applyButtonUrl" value="<%= data.overview.social_info.apply_button.url %>">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Requirements Tab -->
<div class="tab-pane fade" id="requirements" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="requirementsTitle" value="<%= data.requirements.title %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Requirements List</label>
<div id="requirementItems">
<% (data.requirements.items || []).forEach((item, index) => { %>
<div class="mb-3 d-flex gap-2">
<input type="text" class="form-control" value="<%= item %>">
<button type="button" class="btn btn-outline-danger" onclick="removeItem(this)">
<i class="fas fa-times"></i>
</button>
</div>
<% }); %>
</div>
<button type="button" class="btn btn-outline-primary mt-2" onclick="addRequirementItem()">
<i class="fas fa-plus me-1"></i>Add Requirement
</button>
</div>
</div>
</div>
<!-- Action Buttons Tab -->
<div class="tab-pane fade" id="actions" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="actionButtonsTitle" value="<%= data.action_buttons.title %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Buttons</label>
<div id="actionButtonsList">
<% (data.action_buttons.buttons || []).forEach((button, index) => { %>
<div class="mb-3 border p-3">
<div class="row g-2">
<div class="col-md-5">
<label class="form-label">Text</label>
<input type="text" class="form-control" value="<%= button.text %>">
</div>
<div class="col-md-6">
<label class="form-label">Link</label>
<input type="text" class="form-control" value="<%= button.link %>">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger" onclick="removeActionButton(this)">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<% }); %>
</div>
<button type="button" class="btn btn-outline-primary mt-2" onclick="addActionButton()">
<i class="fas fa-plus me-1"></i>Add Button
</button>
</div>
</div>
</div>
<!-- Why Study Tab -->
<div class="tab-pane fade" id="why" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="whyStudyTitle" value="<%= data.why_study.title %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Items</label>
<div id="whyStudyItems">
<% (data.why_study.items || []).forEach((item, index) => { %>
<div class="mb-3 border p-3">
<div class="row g-2">
<div class="col-md-2">
<label class="form-label">Number</label>
<input type="text" class="form-control" value="<%= item.number %>">
</div>
<div class="col-md-4">
<label class="form-label">Title</label>
<input type="text" class="form-control" value="<%= item.title %>">
</div>
<div class="col-md-5">
<label class="form-label">Text</label>
<textarea class="form-control" rows="2"><%= item.text %></textarea>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger" onclick="removeWhyStudyItem(this)">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<% }); %>
</div>
<button type="button" class="btn btn-outline-primary mt-2" onclick="addWhyStudyItem()">
<i class="fas fa-plus me-1"></i>Add Item
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed bottom buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary">
<i class="fas fa-undo me-1"></i>Reset
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Thêm input file ẩn để sử dụng cho upload trực tiếp -->
<input type="file" id="directImageUpload" style="display: none;">
<input type="hidden" id="currentImageType" name="imageType">
<input type="hidden" id="currentTargetInput" name="targetInput">
<script>
// Helper function to create JSON from form data with validation
function createJsonFromForm() {
try {
// Banner
const bannerJson = {
image: document.getElementById('bannerImage')?.value || '',
title: document.getElementById('bannerTitle')?.value || '',
text: document.getElementById('bannerText')?.value || ''
};
document.getElementById('bannerJson').value = JSON.stringify(bannerJson);
// Overview
const overviewJson = {
title: document.getElementById('overviewTitle')?.value || '',
paragraphs: Array.from(document.querySelectorAll('#overviewParagraphs textarea')).map(ta => ta.value || ''),
contact_info: {
title: document.getElementById('contactTitle')?.value || '',
subtitle: document.getElementById('contactSubtitle')?.value || '',
items: Array.from(document.querySelectorAll('#contactItems input')).map(input => ({
text: input.value || ''
}))
},
social_info: {
title: document.getElementById('socialTitle')?.value || '',
social_links: Array.from(document.querySelectorAll('#socialLinks .mb-3')).map(div => ({
image: div.querySelectorAll('input')[0]?.value || '',
url: div.querySelectorAll('input')[1]?.value || '',
alt: div.querySelectorAll('input')[2]?.value || ''
})),
apply_button: {
text: document.getElementById('applyButtonText')?.value || '',
url: document.getElementById('applyButtonUrl')?.value || ''
}
}
};
document.getElementById('overviewJson').value = JSON.stringify(overviewJson);
// Requirements
const requirementsJson = {
title: document.getElementById('requirementsTitle')?.value || '',
items: Array.from(document.querySelectorAll('#requirementItems input')).map(input => input.value || '')
};
document.getElementById('requirementsJson').value = JSON.stringify(requirementsJson);
// Action Buttons
const actionButtonsJson = {
title: document.getElementById('actionButtonsTitle')?.value || '',
buttons: Array.from(document.querySelectorAll('#actionButtonsList .mb-3')).map(div => ({
text: div.querySelectorAll('input')[0]?.value || '',
link: div.querySelectorAll('input')[1]?.value || ''
}))
};
document.getElementById('actionbuttonsJson').value = JSON.stringify(actionButtonsJson);
// Why Study
const whyStudyJson = {
title: document.getElementById('whyStudyTitle')?.value || '',
items: Array.from(document.querySelectorAll('#whyStudyItems .mb-3')).map(div => ({
number: div.querySelectorAll('input')[0]?.value || '',
title: div.querySelectorAll('input')[1]?.value || '',
text: div.querySelector('textarea')?.value || ''
}))
};
document.getElementById('whystudyJson').value = JSON.stringify(whyStudyJson);
return true;
} catch (error) {
console.error('Error creating JSON:', error);
if (window.toastManager) {
window.toastManager.error('Error preparing form data: ' + error.message);
}
return false;
}
}
document.addEventListener('DOMContentLoaded', function() {
// Xử lý upload hình ảnh
document.querySelectorAll('.btn-upload-image').forEach(btn => {
btn.addEventListener('click', function() {
const targetInput = this.getAttribute('data-target-input');
const imageType = this.getAttribute('data-image-type') || 'level';
// Lưu thông tin vào hidden inputs
document.getElementById('currentImageType').value = imageType;
document.getElementById('currentTargetInput').value = targetInput;
// Kích hoạt input file ẩn
document.getElementById('directImageUpload').click();
});
});
// Xử lý khi chọn file
document.getElementById('directImageUpload').addEventListener('change', async function(e) {
if (!this.files || !this.files[0]) return;
const formData = new FormData();
formData.append('image', this.files[0]);
const imageType = document.getElementById('currentImageType').value;
const targetInput = document.getElementById('currentTargetInput').value;
try {
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.path) {
const inputElement = document.getElementById(targetInput);
if (inputElement) {
inputElement.value = result.path;
// Cập nhật hoặc tạo mới hình ảnh preview
let previewContainer = inputElement.closest('.input-group').parentElement.querySelector('.image-preview');
// Tìm container image-preview trong cùng hàng nhưng ở cột bên cạnh
if (!previewContainer) {
previewContainer = inputElement.closest('.row').querySelector('.col-md-6:nth-child(2) .image-preview');
}
// Nếu chưa có container preview thì tạo mới
if (!previewContainer) {
previewContainer = document.createElement('div');
previewContainer.className = 'image-preview mt-2';
const rightColumn = inputElement.closest('.row').querySelector('.col-md-6:nth-child(2)');
if (rightColumn) {
rightColumn.appendChild(previewContainer);
} else {
// Fallback: tạo tại cột hiện tại nếu không tìm thấy cột bên phải
previewContainer = document.createElement('div');
previewContainer.className = 'image-preview mt-2';
inputElement.closest('.input-group').parentElement.appendChild(previewContainer);
}
}
// Tìm hoặc tạo mới thẻ img để preview ảnh
let img = previewContainer.querySelector('img');
if (!img) {
img = document.createElement('img');
img.className = 'img-thumbnail';
previewContainer.appendChild(img);
}
// Thiết lập style cho ảnh preview dựa vào loại ảnh
if (targetInput.toLowerCase().includes('logo')) {
img.style.height = '100px';
img.style.objectFit = 'contain';
img.style.width = '';
} else {
img.style.height = '200px';
img.style.width = '100%';
img.style.objectFit = 'cover';
}
img.src = result.path;
img.alt = 'Image preview';
if (window.toastManager) {
window.toastManager.success('Tải ảnh thành công');
}
}
} else {
if (window.toastManager) {
window.toastManager.error(result.error || 'Error uploading image');
}
}
} catch (error) {
console.error('Upload error:', error);
if (window.toastManager) {
window.toastManager.error('Error uploading image');
}
}
// Reset input để có thể chọn lại cùng một file nếu cần
this.value = '';
});
});
// Form submit handler with validation
document.getElementById('levelForm').addEventListener('submit', function(e) {
e.preventDefault();
try {
// Create and validate JSON data
if (!createJsonFromForm()) {
return;
}
// Validate required fields
const requiredFields = ['bannerJson', 'overviewJson', 'requirementsJson'];
const missingFields = requiredFields.filter(field => {
const value = document.getElementById(field).value;
return !value || value === '{}';
});
if (missingFields.length > 0) {
if (window.toastManager) {
window.toastManager.error(`Missing required data: ${missingFields.join(', ')}`);
}
return;
}
// Show loading toast
if (window.toastManager) {
window.toastManager.info('Saving changes...');
}
this.submit();
} catch (error) {
console.error('Form submission error:', error);
if (window.toastManager) {
window.toastManager.error('Error submitting form: ' + error.message);
}
}
});
// Helper functions for adding/removing items
function addParagraph(containerId) {
const container = document.getElementById(containerId);
const div = document.createElement('div');
div.className = 'mb-3 d-flex gap-2';
div.innerHTML = `
<textarea class="form-control" rows="3"></textarea>
<button type="button" class="btn btn-outline-danger" onclick="removeParagraph(this)">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(div);
}
function removeParagraph(button) {
button.closest('.mb-3').remove();
}
function addRequirementItem() {
const container = document.getElementById('requirementItems');
const div = document.createElement('div');
div.className = 'mb-3 d-flex gap-2';
div.innerHTML = `
<input type="text" class="form-control">
<button type="button" class="btn btn-outline-danger" onclick="removeItem(this)">
<i class="fas fa-times"></i>
</button>
`;
container.appendChild(div);
}
function removeItem(button) {
button.closest('.mb-3').remove();
}
function addActionButton() {
const container = document.getElementById('actionButtonsList');
const div = document.createElement('div');
div.className = 'mb-3 border p-3';
div.innerHTML = `
<div class="row g-2">
<div class="col-md-5">
<label class="form-label">Text</label>
<input type="text" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">Link</label>
<input type="text" class="form-control">
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger" onclick="removeActionButton(this)">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
container.appendChild(div);
}
function removeActionButton(button) {
button.closest('.mb-3').remove();
}
function addWhyStudyItem() {
const container = document.getElementById('whyStudyItems');
const div = document.createElement('div');
div.className = 'mb-3 border p-3';
div.innerHTML = `
<div class="row g-2">
<div class="col-md-2">
<label class="form-label">Number</label>
<input type="text" class="form-control">
</div>
<div class="col-md-4">
<label class="form-label">Title</label>
<input type="text" class="form-control">
</div>
<div class="col-md-5">
<label class="form-label">Text</label>
<textarea class="form-control" rows="2"></textarea>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="button" class="btn btn-outline-danger" onclick="removeWhyStudyItem(this)">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
container.appendChild(div);
}
function removeWhyStudyItem(button) {
button.closest('.mb-3').remove();
}
// (Removed) Helper functions for form/apply/application as Level now references Form only
// Tận dụng module ImageUploader đã được tải trong layout
</script>
<!-- Modal tùy chỉnh xác nhận xóa level type -->
<div id="customModal" class="custom-modal">
<div class="custom-modal-content">
<div class="custom-modal-header">
<h5 class="custom-modal-title">Delete Confirmation</h5>
<button type="button" class="custom-modal-close">&times;</button>
</div>
<div class="custom-modal-body">
<p id="modalMessage">Are you sure you want to delete this level type?</p>
</div>
<div class="custom-modal-footer">
<button type="button" class="btn btn-secondary custom-modal-cancel">Cancel</button>
<button type="button" class="btn btn-danger custom-modal-ok">Delete Permanently</button>
</div>
</div>
</div>
<!-- Import custom modal CSS -->
<link rel="stylesheet" href="/css/custom-modal.css">
<script>
// Custom Modal Functions
document.addEventListener('DOMContentLoaded', function() {
// Khởi tạo modal tùy chỉnh
CustomModal.init('customModal', {
closeOnOutsideClick: true,
animationDuration: 300
});
// Mở modal
document.addEventListener('click', function(e) {
if (e.target.getAttribute('data-custom-modal') === 'open' ||
e.target.parentElement.getAttribute('data-custom-modal') === 'open') {
// Lấy button hoặc icon parent nếu click vào icon
const button = e.target.getAttribute('data-custom-modal') === 'open' ?
e.target : e.target.parentElement;
const type = button.getAttribute('data-type') || '<%= currentType %>';
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
CustomModal.confirm(
`Are you sure you want to delete level type "${type}"? This action cannot be undone. All related data will be permanently deleted.`,
function() {
// Hành động khi xác nhận
window.location.href = `/admin/level/delete/${type}`;
},
null, // Không cần xử lý khi hủy
'Delete Confirmation'
);
}
});
});
</script>