first commit

This commit is contained in:
2026-04-11 14:08:27 +07:00
parent e86e5d2c46
commit 6b7655aa16
389 changed files with 5387 additions and 60861 deletions

View File

@@ -1,967 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on About Us page</p>
</div>
<div>
<a href="<%= frontendUrl %>/about-us/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View About Us Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="aboutUsForm"
action="/admin/about-us/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="heroJson" id="heroJson">
<input type="hidden" name="introJson" id="introJson">
<input type="hidden" name="missionJson" id="missionJson">
<input type="hidden" name="featuresJson" id="featuresJson">
<input type="hidden" name="newsJson" id="newsJson">
<input type="hidden" name="activeTab" id="activeTabInput" value="<%= locals.activeTab || 'hero' %>">
<!-- Navigation Tabs -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<!-- Tab Menu -->
<ul class="nav nav-tabs card-header-tabs" id="aboutUsTabs" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link <%= (locals.activeTab === 'hero' || !locals.activeTab) ? 'active' : '' %>"
id="hero-tab" data-bs-toggle="tab" href="#hero" role="tab"
aria-selected="true"><i class="fas fa-image me-2"></i>Hero</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link <%= locals.activeTab === 'intro' ? 'active' : '' %>" id="intro-tab"
data-bs-toggle="tab" href="#intro" role="tab"
aria-selected="false"><i class="fas fa-info-circle me-2"></i>Intro</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link <%= locals.activeTab === 'mission' ? 'active' : '' %>" id="mission-tab"
data-bs-toggle="tab" href="#mission" role="tab"
aria-selected="false"><i class="fas fa-bullseye me-2"></i>Mission</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link <%= locals.activeTab === 'features' ? 'active' : '' %>" id="features-tab"
data-bs-toggle="tab" href="#features" role="tab"
aria-selected="false"><i class="fas fa-star me-2"></i>Features</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link <%= locals.activeTab === 'news' ? 'active' : '' %>" id="news-tab"
data-bs-toggle="tab" href="#news" role="tab"
aria-selected="false"><i class="fas fa-newspaper me-2"></i>News</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade <%= (locals.activeTab === 'hero' || !locals.activeTab) ? 'show active' : '' %>" id="hero"
role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-image me-2"></i>Hero Section</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Breadcrumb (comma separated)</label>
<input type="text" class="form-control" id="heroBreadcrumb"
value="<%= (data.hero?.breadcrumb || []).join(', ') %>">
</div>
<div class="col-md-12">
<label class="form-label">Background Image</label>
<div class="input-group">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage" value="<%= data.hero?.backgroundImage || '' %>">
<button class="btn btn-outline-primary btn-upload-image" type="button"
data-target-input="heroBackgroundImage" data-image-type="about">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.hero?.backgroundImage) { %>
<img src="<%= data.hero.backgroundImage %>"
class="img-thumbnail uploaded-preview mt-2"
style="max-height: 200px;">
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Intro Tab -->
<div class="tab-pane fade <%= locals.activeTab === 'intro' ? 'show active' : '' %>" id="intro"
role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-info-circle me-2"></i>Introduction</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Subheading</label>
<input type="text" class="form-control" id="introSubheading" name="introSubheading"
value="<%= data.intro?.subheading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Heading</label>
<input type="text" class="form-control" id="introHeading" name="introHeading"
value="<%= data.intro?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control" id="introDescription" name="introDescription"
rows="4"><%= data.intro?.description || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label">Main Image</label>
<div class="input-group">
<input type="text" class="form-control" id="introImage" name="introImage"
value="<%= data.intro?.image || '' %>">
<button class="btn btn-outline-primary btn-upload-image" type="button"
data-target-input="introImage" data-image-type="about">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.intro?.image) { %>
<img src="<%= data.intro.image %>"
class="img-thumbnail uploaded-preview mt-2"
style="max-height: 200px;">
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Mission Tab -->
<div class="tab-pane fade <%= locals.activeTab === 'mission' ? 'show active' : '' %>" id="mission" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-bullseye me-2"></i>Mission Section</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Subheading</label>
<input type="text" class="form-control" id="missionSubheading" value="<%= data.mission?.subheading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Heading</label>
<input type="text" class="form-control" id="missionHeading" value="<%= data.mission?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control" id="missionDescription" rows="3"><%= data.mission?.description || '' %></textarea>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label">CTA Button Label</label>
<input type="text" class="form-control" id="missionCtaLabel" value="<%= data.mission?.ctaButton?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">CTA Button Link</label>
<input type="text" class="form-control" id="missionCtaHref" value="<%= data.mission?.ctaButton?.href || '' %>">
</div>
</div>
<h6 class="mt-4 mb-3">Images</h6>
<div class="row g-3">
<% ['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(imgKey => { %>
<div class="col-md-4">
<label class="form-label"><%= imgKey.charAt(0).toUpperCase() + imgKey.slice(1) %></label>
<div class="input-group">
<input type="text" class="form-control" id="missionImg_<%= imgKey %>" value="<%= data.mission?.images?.[imgKey] || '' %>">
<button class="btn btn-outline-primary btn-upload-image btn-sm" type="button" data-target-input="missionImg_<%= imgKey %>" data-image-type="about">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
<% }) %>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Items (Icons & Labels)</label>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionItem()">
<i class="fas fa-plus me-1"></i>Add
</button>
</div>
<div id="missionItemsContainer"></div>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Features (List)</label>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMissionFeature()">
<i class="fas fa-plus me-1"></i>Add
</button>
</div>
<div id="missionFeaturesContainer"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Features Tab -->
<div class="tab-pane fade <%= locals.activeTab === 'features' ? 'show active' : '' %>" id="features" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Features Section</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Subheading</label>
<input type="text" class="form-control" id="featuresSubheading" value="<%= data.features?.subheading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Heading</label>
<input type="text" class="form-control" id="featuresHeading" value="<%= data.features?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control" id="featuresDescription" rows="3"><%= data.features?.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label">Background Image</label>
<div class="input-group">
<input type="text" class="form-control" id="featuresBgImage" value="<%= data.features?.backgroundImage || '' %>">
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featuresBgImage" data-image-type="about">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Side Image</label>
<div class="input-group">
<input type="text" class="form-control" id="featuresImage" value="<%= data.features?.image || '' %>">
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featuresImage" data-image-type="about">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label">CTA Button Label</label>
<input type="text" class="form-control" id="featuresCtaLabel" value="<%= data.features?.ctaButton?.label || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">CTA Button Link</label>
<input type="text" class="form-control" id="featuresCtaHref" value="<%= data.features?.ctaButton?.href || '' %>">
</div>
</div>
<div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Feature Items</h6>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addFeatureItem()">
<i class="fas fa-plus me-1"></i>Add Item
</button>
</div>
<div id="featureItemsContainer"></div>
</div>
</div>
</div>
</div>
<!-- News Tab -->
<div class="tab-pane fade <%= locals.activeTab === 'news' ? 'show active' : '' %>" id="news" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="fas fa-newspaper me-2"></i>News Section (Blog Preview)</h6>
<span class="badge bg-info text-dark">System will automatically fetch the 3 latest posts if no specific blog is selected.</span>
</div>
<div class="card-body p-4">
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="newsSubheading" value="<%= data.news?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="newsHeading" value="<%= data.news?.heading || '' %>" placeholder="e.g., Latest Insights & Updates">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">CTA Button Label</label>
<input type="text" class="form-control" id="newsCtaLabel" value="<%= data.news?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">CTA Button Link</label>
<input type="text" class="form-control" id="newsCtaHref" value="<%= data.news?.ctaButton?.href || '' %>" placeholder="/blog">
</div>
</div>
<div class="col-md-12 mt-4">
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct from Blog Module)</label>
<p class="text-muted small mb-3">Select blog posts to display on About page. If none are selected, the system will use the 3 latest posts.</p>
<div class="row g-3 blog-selector-container" style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
<% if (allBlogs && allBlogs.length > 0) { %>
<% allBlogs.forEach(blog => {
const isSelected = data.news?.selectedBlogIds && data.news.selectedBlogIds.some(id => id.toString() === blog._id.toString());
%>
<div class="col-md-4">
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>" onclick="toggleAboutBlogSelection(this, '<%= blog._id %>')" style="cursor: pointer; transition: all 0.2s;">
<div class="position-absolute top-0 end-0 m-2">
<div class="form-check">
<input class="form-check-input about-blog-checkbox" type="checkbox" value="<%= blog._id %>" <%= isSelected ? 'checked' : '' %> onclick="event.stopPropagation(); handleAboutCheckboxChange(this)">
</div>
</div>
<img src="<%= blog.featuredImage ? (blog.featuredImage.startsWith('http') ? blog.featuredImage : backendUrl + blog.featuredImage) : '/assets/img/placeholder.jpg' %>" class="card-img-top" style="height: 210px; object-fit: cover;">
<div class="card-body p-2">
<h6 class="card-title small fw-bold mb-1" title="<%= blog.title %>" style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.6em; line-height: 1.3em;">
<%= blog.title %>
</h6>
<p class="card-text tiny text-muted mb-0">
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
</p>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="col-12 text-center py-4">
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons inside Card Footer -->
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
<button type="button" class="btn btn-outline-secondary px-4" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-outline-primary px-4" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
originalFormData = <%- JSON.stringify(data) %>;
updateAllJsonInputs(originalFormData);
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('aboutUsForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
const originalHtml = submitBtn.innerHTML;
try {
// Collect all data into a single object
updateJsonData();
// No more "Saving..." text or disabling button to keep UI "instant"
// submitBtn.disabled = true;
// submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
// We'll send the individual section JSONs or construct one big one
const aboutData = {
hero: JSON.parse(document.getElementById('heroJson').value),
intro: JSON.parse(document.getElementById('introJson').value),
mission: JSON.parse(document.getElementById('missionJson').value),
features: JSON.parse(document.getElementById('featuresJson').value),
news: JSON.parse(document.getElementById('newsJson').value)
};
const response = await fetch('/api/about', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ aboutJson: JSON.stringify(aboutData) })
});
if (!response.ok) throw new Error('Update failed');
const result = await response.json();
if (result.success) {
showToast('Success', 'About Us updated successfully', 'success');
// Update the local state with returned data from server
// This ensures the UI is in sync with what was actually saved
if (result.data) {
originalFormData = result.data;
updateAllJsonInputs(originalFormData);
}
} else {
throw new Error(result.error || 'Failed to update');
}
} catch (error) {
console.error('Error:', error);
showToast('Error', error.message, 'error');
}
});
document.body.addEventListener('click', function (e) {
const uploadBtn = e.target.closest('.btn-upload-image');
if (uploadBtn) {
const targetInput = uploadBtn.dataset.targetInput;
const imageType = uploadBtn.dataset.imageType;
openImageUploader(targetInput, imageType);
}
});
// Tab change listener to keep track of active tab
const tabs = document.querySelectorAll('a[data-bs-toggle="tab"]');
tabs.forEach(tab => {
tab.addEventListener('shown.bs.tab', function (e) {
const targetId = e.target.getAttribute('href').replace('#', '');
const activeTabInput = document.getElementById('activeTabInput');
if (activeTabInput) {
activeTabInput.value = targetId;
}
});
});
}
function openImageUploader(targetInput, imageType) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
function getPreviewDims(name) {
if (/hero/i.test(name)) return { h: '250px', w: '100%' };
if (/intro/i.test(name) || /mission/i.test(name) || /features/i.test(name)) return { h: '150px', w: '100%' };
return { h: '120px', w: '100%' };
}
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('image', file);
// Disable upload button during upload
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
if (uploadBtn) {
uploadBtn.disabled = true;
}
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');
}
// Update input value
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
if (!input) {
throw new Error('Target input not found');
}
// Use absolute URL for preview when necessary
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://'))) ? result.path : (window.location.origin + result.path);
input.value = result.path;
// Try to find an existing image preview inside the closest card or input group (prefer .uploaded-preview)
let card = input.closest('.card');
let previewImg = card ? card.querySelector('.uploaded-preview') : null;
if (!previewImg) {
// Look for a preview right after the input group
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
}
const dims = getPreviewDims(targetInput);
if (previewImg) {
previewImg.src = previewUrl;
previewImg.style.height = dims.h;
previewImg.style.width = dims.w;
} else {
// Create a preview image and attach it after the input group
const img = document.createElement('img');
img.src = previewUrl;
img.className = 'img-thumbnail uploaded-preview mt-2';
img.style.height = dims.h;
img.style.width = dims.w;
img.style.objectFit = 'cover';
img.alt = 'Image preview';
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
if (parent) parent.appendChild(img);
}
// Removed toast for silent upload
// Restore button state
if (uploadBtn) {
uploadBtn.disabled = false;
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
// Restore button state
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadBtn) {
uploadBtn.disabled = false;
}
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
updateAllJsonInputs(originalFormData);
showToast('Reset', 'Form restored to last saved state', 'info');
}
}
function showError(message) {
const alertHtml = `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
document.querySelector('.container').insertAdjacentHTML('afterbegin', alertHtml);
}
// Show toast message
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}:</strong> ${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
// Add toast to container
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
}
container.appendChild(toast);
// Show toast
const bsToast = new bootstrap.Toast(toast, {
animation: true,
autohide: true,
delay: 3000
});
bsToast.show();
// Remove toast after hide
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
function updateAllJsonInputs(data) {
if (!data) return;
// 1. Hero
const hero = data.hero || {};
document.getElementById('heroJson').value = JSON.stringify(hero);
document.getElementById('heroTitle').value = hero.title || '';
document.getElementById('heroBreadcrumb').value = (hero.breadcrumb || []).join(', ');
document.getElementById('heroBackgroundImage').value = hero.backgroundImage || '';
updateImagePreview('heroBackgroundImage', hero.backgroundImage);
// 2. Intro
const intro = data.intro || {};
document.getElementById('introJson').value = JSON.stringify(intro);
document.getElementById('introSubheading').value = intro.subheading || '';
document.getElementById('introHeading').value = intro.heading || '';
document.getElementById('introDescription').value = intro.description || '';
document.getElementById('introImage').value = intro.image || '';
updateImagePreview('introImage', intro.image);
// 3. Mission
const mission = data.mission || {};
document.getElementById('missionJson').value = JSON.stringify(mission);
document.getElementById('missionSubheading').value = mission.subheading || '';
document.getElementById('missionHeading').value = mission.heading || '';
document.getElementById('missionDescription').value = mission.description || '';
document.getElementById('missionCtaLabel').value = mission.ctaButton?.label || '';
document.getElementById('missionCtaHref').value = mission.ctaButton?.href || '';
['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(k => {
const el = document.getElementById('missionImg_' + k);
const val = mission.images?.[k] || '';
if (el) {
el.value = val;
updateImagePreview('missionImg_' + k, val);
}
});
populateMissionItems(mission.items || []);
populateMissionFeatures(mission.features || []);
// 4. Features
const features = data.features || {};
document.getElementById('featuresJson').value = JSON.stringify(features);
document.getElementById('featuresSubheading').value = features.subheading || '';
document.getElementById('featuresHeading').value = features.heading || '';
document.getElementById('featuresDescription').value = features.description || '';
document.getElementById('featuresBgImage').value = features.backgroundImage || '';
document.getElementById('featuresImage').value = features.image || '';
document.getElementById('featuresCtaLabel').value = features.ctaButton?.label || '';
document.getElementById('featuresCtaHref').value = features.ctaButton?.href || '';
updateImagePreview('featuresBgImage', features.backgroundImage);
updateImagePreview('featuresImage', features.image);
populateFeatureItems(features.items || []);
// 5. News
const news = data.news || {};
document.getElementById('newsJson').value = JSON.stringify(news);
document.getElementById('newsSubheading').value = news.subheading || '';
document.getElementById('newsHeading').value = news.heading || '';
document.getElementById('newsCtaLabel').value = news.ctaButton?.label || '';
document.getElementById('newsCtaHref').value = news.ctaButton?.href || '';
// Update blog selection checkboxes
document.querySelectorAll('.about-blog-checkbox').forEach(cb => {
const isSelected = news.selectedBlogIds && news.selectedBlogIds.some(id => id.toString() === cb.value);
cb.checked = isSelected;
const card = cb.closest('.blog-select-card');
if (card) {
handleAboutCheckboxUpdate(card, isSelected);
}
});
}
function updateImagePreview(inputId, imagePath) {
if (!imagePath) return;
const input = document.getElementById(inputId);
if (!input) return;
let card = input.closest('.card');
let previewImg = card ? card.querySelector('.uploaded-preview') : null;
if (!previewImg) {
const parent = input.closest('.input-group') || input.parentElement;
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
}
if (previewImg) {
previewImg.src = imagePath;
}
}
// --- Helper dynamic populations ---
function addMissionItem() {
const container = document.getElementById('missionItemsContainer');
const idx = container.children.length;
const html = `
<div class="card mb-2 mission-item">
<div class="card-body p-2">
<div class="row g-2">
<div class="col-md-4">
<div class="input-group input-group-sm">
<input type="text" class="form-control" name="missionItemIcon_${idx}" placeholder="Icon path">
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="missionItemIcon_${idx}" data-image-type="about">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
<div class="col-md-4">
<input type="text" class="form-control form-control-sm" name="missionItemLabel_${idx}" placeholder="Label">
</div>
<div class="col-md-4">
<input type="text" class="form-control form-control-sm" name="missionItemDesc_${idx}" placeholder="Description">
</div>
</div>
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.mission-item').remove()">Remove</button>
</div>
</div>`;
container.insertAdjacentHTML('beforeend', html);
}
function populateMissionItems(items) {
const container = document.getElementById('missionItemsContainer');
container.innerHTML = '';
items.forEach((item, i) => {
addMissionItem();
const last = container.lastElementChild;
last.querySelector(`[name="missionItemIcon_${i}"]`).value = item.icon || '';
last.querySelector(`[name="missionItemLabel_${i}"]`).value = item.label || '';
last.querySelector(`[name="missionItemDesc_${i}"]`).value = item.description || '';
});
}
function addMissionFeature() {
const container = document.getElementById('missionFeaturesContainer');
const html = `
<div class="input-group input-group-sm mb-1">
<input type="text" class="form-control mission-feature-input" placeholder="Feature text">
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()">x</button>
</div>`;
container.insertAdjacentHTML('beforeend', html);
}
function populateMissionFeatures(features) {
const container = document.getElementById('missionFeaturesContainer');
container.innerHTML = '';
features.forEach(f => {
addMissionFeature();
container.lastElementChild.querySelector('input').value = f || '';
});
}
function addFeatureItem() {
const container = document.getElementById('featureItemsContainer');
const idx = container.children.length;
const html = `
<div class="card mb-2 feature-item-row">
<div class="card-body p-2">
<div class="row g-2">
<div class="col-md-4">
<div class="input-group input-group-sm">
<input type="text" class="form-control" name="featureItemIcon_${idx}" placeholder="Icon path">
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="featureItemIcon_${idx}" data-image-type="about">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
<div class="col-md-4">
<input type="text" class="form-control form-control-sm" name="featureItemTitle_${idx}" placeholder="Title">
</div>
<div class="col-md-4">
<input type="text" class="form-control form-control-sm" name="featureItemDesc_${idx}" placeholder="Description">
</div>
</div>
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-1" onclick="this.closest('.feature-item-row').remove()">Remove</button>
</div>
</div>`;
container.insertAdjacentHTML('beforeend', html);
}
function populateFeatureItems(items) {
const container = document.getElementById('featureItemsContainer');
container.innerHTML = '';
items.forEach((item, i) => {
addFeatureItem();
const last = container.lastElementChild;
last.querySelector(`[name="featureItemIcon_${i}"]`).value = item.icon || '';
last.querySelector(`[name="featureItemTitle_${i}"]`).value = item.title || '';
last.querySelector(`[name="featureItemDesc_${i}"]`).value = item.description || '';
});
}
// Blog selection functions for About News section
function toggleAboutBlogSelection(card, blogId) {
const checkbox = card.querySelector('.about-blog-checkbox');
const isChecking = !checkbox.checked;
if (isChecking) {
const checkedCount = document.querySelectorAll('.about-blog-checkbox:checked').length;
if (checkedCount >= 3) {
alert('You can only select up to 3 blogs.');
return;
}
}
checkbox.checked = isChecking;
handleAboutCheckboxUpdate(card, checkbox.checked);
}
function handleAboutCheckboxChange(checkbox) {
if (checkbox.checked) {
const checkedCount = document.querySelectorAll('.about-blog-checkbox:checked').length;
if (checkedCount > 3) {
checkbox.checked = false;
alert('You can only select up to 3 blogs.');
return;
}
}
const card = checkbox.closest('.blog-select-card');
handleAboutCheckboxUpdate(card, checkbox.checked);
}
function handleAboutCheckboxUpdate(card, isChecked) {
if (isChecked) {
card.classList.add('border-primary', 'bg-light');
} else {
card.classList.remove('border-primary', 'bg-light');
}
}
function addService() {
const container = document.getElementById('servicesContainer');
const index = container.children.length;
const html = `
<div class="card mb-3 service-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Title</label>
<input type="text" class="form-control" name="serviceTitle_${index}">
</div>
<div class="col-md-8">
<label class="form-label">Description</label>
<textarea class="form-control" name="serviceDescription_${index}" rows="2"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeService(this)">
<i class="fas fa-trash me-2"></i>Remove Service
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function updateJsonData() {
try {
// Hero
const heroData = {
title: document.getElementById('heroTitle').value.trim(),
breadcrumb: document.getElementById('heroBreadcrumb').value.split(',').map(s => s.trim()).filter(s => s !== ''),
backgroundImage: document.getElementById('heroBackgroundImage').value.trim()
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Intro
const introData = {
subheading: document.getElementById('introSubheading').value.trim(),
heading: document.getElementById('introHeading').value.trim(),
description: document.getElementById('introDescription').value.trim(),
image: document.getElementById('introImage').value.trim()
};
document.getElementById('introJson').value = JSON.stringify(introData);
// Mission
const missionData = {
subheading: document.getElementById('missionSubheading').value.trim(),
heading: document.getElementById('missionHeading').value.trim(),
description: document.getElementById('missionDescription').value.trim(),
images: {
main: document.getElementById('missionImg_main').value.trim(),
secondary: document.getElementById('missionImg_secondary').value.trim(),
bgShape: document.getElementById('missionImg_bgShape').value.trim(),
planeShape: document.getElementById('missionImg_planeShape').value.trim(),
topShape: document.getElementById('missionImg_topShape').value.trim(),
globeShape: document.getElementById('missionImg_globeShape').value.trim()
},
items: Array.from(document.querySelectorAll('.mission-item')).map(item => ({
icon: item.querySelector('[name^="missionItemIcon_"]').value.trim(),
label: item.querySelector('[name^="missionItemLabel_"]').value.trim(),
description: item.querySelector('[name^="missionItemDesc_"]').value.trim()
})).filter(i => i.label !== ''),
features: Array.from(document.querySelectorAll('.mission-feature-input')).map(input => input.value.trim()).filter(v => v !== ''),
ctaButton: {
label: document.getElementById('missionCtaLabel').value.trim(),
href: document.getElementById('missionCtaHref').value.trim()
}
};
document.getElementById('missionJson').value = JSON.stringify(missionData);
// Features
const featuresData = {
backgroundImage: document.getElementById('featuresBgImage').value.trim(),
subheading: document.getElementById('featuresSubheading').value.trim(),
heading: document.getElementById('featuresHeading').value.trim(),
description: document.getElementById('featuresDescription').value.trim(),
image: document.getElementById('featuresImage').value.trim(),
items: Array.from(document.querySelectorAll('.feature-item-row')).map(item => ({
icon: item.querySelector('[name^="featureItemIcon_"]').value.trim(),
title: item.querySelector('[name^="featureItemTitle_"]').value.trim(),
description: item.querySelector('[name^="featureItemDesc_"]').value.trim()
})).filter(i => i.title !== ''),
ctaButton: {
label: document.getElementById('featuresCtaLabel').value.trim(),
href: document.getElementById('featuresCtaHref').value.trim()
}
};
document.getElementById('featuresJson').value = JSON.stringify(featuresData);
// News
const selectedIds = [];
document.querySelectorAll('.about-blog-checkbox:checked').forEach(cb => {
selectedIds.push(cb.value);
});
const newsData = {
subheading: document.getElementById('newsSubheading').value.trim(),
heading: document.getElementById('newsHeading').value.trim(),
ctaButton: {
label: document.getElementById('newsCtaLabel').value.trim(),
href: document.getElementById('newsCtaHref').value.trim()
},
selectedBlogIds: selectedIds,
items: [] // Server will populate this from selectedBlogIds
};
document.getElementById('newsJson').value = JSON.stringify(newsData);
} catch (error) {
console.error('Error updating JSON data:', error);
throw new Error('Failed to process form data');
}
}
</script>
<style>
.tiny {
font-size: 0.75rem;
}
.blog-select-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,791 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on Appointment page</p>
</div>
<div>
<a href="<%= frontendUrl %>/make-appointment/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Appointment Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="appointmentForm"
action="/admin/appointment/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="visaOptions" id="visaOptionsJson">
<input type="hidden" name="form" id="formJson">
<!-- 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="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#visaOptions" role="tab">
<i class="fas fa-passport me-2"></i>Visa Options
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
<i class="fas fa-envelope me-2"></i>Form
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#submissions" role="tab">
<i class="fas fa-list me-2"></i>Submissions
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Hero Section</h6>
<div class="row g-3">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="appointment">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px;">
<% if (data.hero?.backgroundImage) { %>
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
!heroImgSrc.startsWith('http://') &&
!heroImgSrc.startsWith('https://')) {
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
heroImgSrc; } %>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="heroSubtitle"
name="heroSubtitle" value="<%= data.hero?.subtitle || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="heroHeading"
name="heroHeading" value="<%= data.hero?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="heroDescription"
name="heroDescription"
rows="2"><%= data.hero?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Visa Options Tab -->
<div class="tab-pane fade" id="visaOptions" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Visa Options</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addVisaOption()">
<i class="fas fa-plus"></i> Add Option
</button>
</div>
<p class="text-muted small">These options will appear in the visa type selection
dropdown on the appointment form.</p>
<div id="visaOptionsContainer">
<% if (data.visaOptions && data.visaOptions.length> 0) { %>
<% data.visaOptions.forEach((option, index)=> { %>
<div class="input-group mb-2 visa-option-item">
<span class="input-group-text"><i
class="fas fa-passport"></i></span>
<input type="text" class="form-control visa-option-input"
value="<%= option %>" placeholder="Enter visa option">
<button type="button" class="btn btn-outline-danger"
onclick="removeVisaOption(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Form Tab -->
<div class="tab-pane fade" id="form" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Form Settings</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Form Heading</label>
<input type="text" class="form-control" id="formHeading"
value="<%= data.form?.heading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Submit Button Text</label>
<input type="text" class="form-control" id="formSubmitButtonText"
value="<%= data.form?.submitButton?.text || 'Request Appointment' %>">
</div>
<!-- Hidden fields for submitButton icon and buttonClass -->
<input type="hidden" id="formSubmitButtonIcon"
value="<%= data.form?.submitButton?.icon || 'fa-solid fa-arrow-right' %>">
<input type="hidden" id="formSubmitButtonClass"
value="<%= data.form?.submitButton?.buttonClass || 'theme-btn' %>">
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Form Fields</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addFormField()">
<i class="fas fa-plus"></i> Add Field
</button>
</div>
<div id="formFieldsContainer">
<% if (data.form?.fields && data.form.fields.length> 0) { %>
<% data.form.fields.forEach((field, index)=> { %>
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text"
class="form-control field-name-input"
value="<%= field.name || '' %>"
placeholder="e.g., name">
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text"
class="form-control field-label-input"
value="<%= field.label || '' %>"
placeholder="e.g., Your Name">
</div>
<div class="col-md-2">
<label class="form-label">Type</label>
<select class="form-select field-type-select">
<option value="text" <%=field.type==='text'
? 'selected' : '' %>>Text</option>
<option value="email" <%=field.type==='email'
? 'selected' : '' %>>Email</option>
<option value="tel" <%=field.type==='tel'
? 'selected' : '' %>>Phone</option>
<option value="textarea"
<%=field.type==='textarea' ? 'selected' : ''
%>>Textarea</option>
<option value="date" <%=field.type==='date'
? 'selected' : '' %>>Date</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Col Class</label>
<select class="form-select field-col-select">
<option value="col-lg-4"
<%=field.colClass==='col-lg-4' ? 'selected'
: '' %>>1/3 Width</option>
<option value="col-lg-6"
<%=field.colClass==='col-lg-6' ? 'selected'
: '' %>>1/2 Width</option>
<option value="col-lg-12"
<%=field.colClass==='col-lg-12' ? 'selected'
: '' %>>Full Width</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input
class="form-check-input field-required-check"
type="checkbox" <%=field.required
? 'checked' : '' %>>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text"
class="form-control field-placeholder-input"
value="<%= field.placeholder || '' %>">
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Submissions Tab -->
<div class="tab-pane fade" id="submissions" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Recent Submissions</h6>
</div>
<!-- Date Filter -->
<div class="row g-2 mb-4 align-items-end" id="filterContainer">
<input type="hidden" id="filterTab" value="submissions">
<div class="col-md-3">
<label class="form-label small text-muted">Start Date</label>
<input type="date" class="form-control form-control-sm"
id="filterStartDate" value="<%= locals.startDate || '' %>">
</div>
<div class="col-md-3">
<label class="form-label small text-muted">End Date</label>
<input type="date" class="form-control form-control-sm"
id="filterEndDate" value="<%= locals.endDate || '' %>">
</div>
<div class="col-md-3">
<button type="button" class="btn btn-sm btn-primary w-100"
onclick="applyDateFilter()">
<i class="fas fa-filter me-1"></i> Filter
</button>
</div>
<div class="col-md-2">
<a href="/admin/appointment?tab=submissions"
class="btn btn-sm btn-outline-secondary w-100">
<i class="fas fa-times me-1"></i> Clear
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Name</th>
<th>Contact</th>
<th>Appt Date</th>
<th>Visa Types</th>
<th>Message</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% if (locals.submissions && submissions.length> 0) { %>
<% submissions.forEach(submission=> { %>
<tr>
<td>
<%= new
Date(submission.createdAt).toLocaleDateString()
%>
<br>
<small class="text-muted">
<%= new
Date(submission.createdAt).toLocaleTimeString([],
{hour: '2-digit' , minute:'2-digit'}) %>
</small>
</td>
<td>
<%= submission.name %>
</td>
<td>
<div class="d-flex flex-column">
<a href="mailto:<%= submission.email %>"
class="text-decoration-none"><i
class="fas fa-envelope me-1"></i>
<%= submission.email %>
</a>
<% if(submission.phone) { %>
<span class="text-muted small"><i
class="fas fa-phone me-1"></i>
<%= submission.phone %>
</span>
<% } %>
</div>
</td>
<td>
<%= submission.appointmentDate || '-' %>
</td>
<td>
<% if (submission.visaTypes &&
submission.visaTypes.length> 0) { %>
<% submission.visaTypes.forEach(type=> { %>
<span
class="badge bg-light text-dark border me-1">
<%= type %>
</span>
<% }); %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (submission.message) { %>
<div title="<%= submission.message %>"
style="max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<%= submission.message %>
</div>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% let statusClass='bg-secondary' ;
if(submission.status==='pending' )
statusClass='bg-warning text-dark' ;
if(submission.status==='confirmed' )
statusClass='bg-success' ;
if(submission.status==='completed' )
statusClass='bg-info text-dark' ;
if(submission.status==='cancelled' )
statusClass='bg-danger' ; %>
<span
class="badge <%= statusClass %> rounded-pill">
<%= submission.status %>
</span>
</td>
<td>
<button type="button"
class="btn btn-sm btn-outline-primary"
onclick="openStatusModal('<%= submission._id %>', '<%= submission.status %>')"
title="Update Status">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="8"
class="text-center py-4 text-muted">No
submissions found</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<div class="mt-3 text-end">
<small class="text-muted">Showing last 50 submissions</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Status Update Modal -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="statusForm">
<input type="hidden" id="statusSubmissionId">
<div class="mb-3">
<label for="statusSelect" class="form-label">Status</label>
<select class="form-select" id="statusSelect">
<option value="pending">Pending</option>
<option value="confirmed">Confirmed</option>
<option value="completed">Completed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="saveStatus()">Save changes</button>
</div>
</div>
</div>
</div>
<script type="application/json" id="appointmentDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
let statusModal = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('appointmentDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
// Check for tab parameter in URL
const urlParams = new URLSearchParams(window.location.search);
const tab = urlParams.get('tab');
if (tab) {
const triggerEl = document.querySelector(`a[href="#${tab}"]`);
if (triggerEl) {
const tabInstance = new bootstrap.Tab(triggerEl);
tabInstance.show();
}
}
// Move modal to body to prevent backdrop issues
const statusModalEl = document.getElementById('statusModal');
if (statusModalEl) {
document.body.appendChild(statusModalEl);
}
statusModal = new bootstrap.Modal(statusModalEl);
updateAllJsonInputs();
initializeFormHandlers();
});
function applyDateFilter() {
const startDate = document.getElementById('filterStartDate').value;
const endDate = document.getElementById('filterEndDate').value;
const url = new URL(window.location.href);
url.searchParams.set('tab', 'submissions');
if (startDate) {
url.searchParams.set('startDate', startDate);
} else {
url.searchParams.delete('startDate');
}
if (endDate) {
url.searchParams.set('endDate', endDate);
} else {
url.searchParams.delete('endDate');
}
window.location.href = url.toString();
}
function openStatusModal(id, currentStatus) {
document.getElementById('statusSubmissionId').value = id;
document.getElementById('statusSelect').value = currentStatus;
statusModal.show();
}
async function saveStatus() {
const id = document.getElementById('statusSubmissionId').value;
const status = document.getElementById('statusSelect').value;
try {
const response = await fetch(`/admin/appointments/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ status })
});
const result = await response.json();
if (result.success) {
// Determine CSS class for the notification or badge
// Since this is generic, we'll reload or update UI manually if complex.
// Reload is safest to show updated table state (including sorting/filtering if any)
// But let's try to be smooth:
window.location.reload();
} else {
alert('Failed to update status: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Error updating status:', error);
alert('Error updating status');
}
}
function initializeFormHandlers() {
const form = document.getElementById('appointmentForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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);
});
});
// Update preview when background image changes
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
updateHeroImagePreview(this.value);
});
}
function updateHeroImagePreview(imagePath) {
const previewContainer = document.getElementById('heroImagePreview');
if (imagePath) {
let imgSrc = imagePath;
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
}
previewContainer.innerHTML = `
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewContainer.innerHTML = `
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
function updateAllJsonInputs() {
updateJsonData();
}
function updateJsonData() {
// Hero data
const heroData = {
title: document.getElementById('heroTitle').value || '',
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
subtitle: document.getElementById('heroSubtitle').value || '',
heading: document.getElementById('heroHeading').value || '',
description: document.getElementById('heroDescription').value || '',
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Visa options
const visaOptions = [];
document.querySelectorAll('.visa-option-input').forEach(input => {
if (input.value.trim()) {
visaOptions.push(input.value.trim());
}
});
document.getElementById('visaOptionsJson').value = JSON.stringify(visaOptions);
// Form data
const fields = [];
document.querySelectorAll('.form-field-item').forEach(item => {
fields.push({
name: item.querySelector('.field-name-input').value || '',
label: item.querySelector('.field-label-input').value || '',
type: item.querySelector('.field-type-select').value || 'text',
placeholder: item.querySelector('.field-placeholder-input').value || '',
required: item.querySelector('.field-required-check').checked,
colClass: item.querySelector('.field-col-select').value || 'col-lg-12',
});
});
const formData = {
heading: document.getElementById('formHeading').value || '',
fields: fields,
submitButton: {
text: document.getElementById('formSubmitButtonText').value || 'Request Appointment',
icon: document.getElementById('formSubmitButtonIcon').value || 'fa-solid fa-arrow-right',
buttonClass: document.getElementById('formSubmitButtonClass').value || 'theme-btn',
},
};
document.getElementById('formJson').value = JSON.stringify(formData);
}
function addVisaOption() {
const container = document.getElementById('visaOptionsContainer');
const html = `
<div class="input-group mb-2 visa-option-item">
<span class="input-group-text"><i class="fas fa-passport"></i></span>
<input type="text" class="form-control visa-option-input" value="" placeholder="Enter visa option">
<button type="button" class="btn btn-outline-danger" onclick="removeVisaOption(this)">
<i class="fas fa-trash"></i>
</button>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeVisaOption(button) {
button.closest('.visa-option-item').remove();
}
function addFormField() {
const container = document.getElementById('formFieldsContainer');
const html = `
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text" class="form-control field-name-input" value="" placeholder="e.g., name">
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text" class="form-control field-label-input" value="" placeholder="e.g., Your Name">
</div>
<div class="col-md-2">
<label class="form-label">Type</label>
<select class="form-select field-type-select">
<option value="text" selected>Text</option>
<option value="email">Email</option>
<option value="tel">Phone</option>
<option value="textarea">Textarea</option>
<option value="date">Date</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Col Class</label>
<select class="form-select field-col-select">
<option value="col-lg-4">1/3 Width</option>
<option value="col-lg-6">1/2 Width</option>
<option value="col-lg-12" selected>Full Width</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input class="form-check-input field-required-check" type="checkbox">
</div>
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control field-placeholder-input" value="">
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFormField(button) {
button.closest('.form-field-item').remove();
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function (reuse from shared)
function openImageUploader(targetInput, imageType) {
// Open upload modal or trigger file input
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('/admin/upload/image', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.imagePath) {
document.getElementById(targetInput).value = result.imagePath;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.imagePath);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>

View File

@@ -1,699 +1,243 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">System activity and change tracking</p>
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Audit Logs</h1>
<p class="subtitle">System activity and change tracking</p>
</div>
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
<i class="fas fa-broom"></i> Cleanup Old Logs
</button>
</div>
<!-- Filters -->
<div class="card border-0 mb-3">
<div class="card-body" style="padding:1rem 1.25rem;">
<form method="GET" action="/admin/audit-logs">
<div class="row g-2 align-items-end">
<div class="col-md-2">
<label class="form-label">Model</label>
<select class="form-select" name="model">
<option value="">All Models</option>
<% uniqueModels.forEach(model => { %>
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>><%= model %></option>
<% }); %>
</select>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-warning" onclick="openCleanupModal()">
<i class="fas fa-broom me-2"></i>Cleanup Old Logs
</button>
<div class="col-md-2">
<label class="form-label">Action</label>
<select class="form-select" name="action">
<option value="">All Actions</option>
<% uniqueActions.forEach(action => { %>
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>><%= action %></option>
<% }); %>
</select>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" action="/admin/audit-logs" class="row g-3">
<div class="col-md-2">
<label class="form-label">Model</label>
<select class="form-select" name="model">
<option value="">All Models</option>
<% uniqueModels.forEach(model => { %>
<option value="<%= model %>" <%= query.model === model ? 'selected' : '' %>>
<%= model %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Action</label>
<select class="form-select" name="action">
<option value="">All Actions</option>
<% uniqueActions.forEach(action => { %>
<option value="<%= action %>" <%= query.action === action ? 'selected' : '' %>>
<%= action %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">User</label>
<select class="form-select" name="user">
<option value="">All Users</option>
<% users.forEach(user => { %>
<option value="<%= user._id %>" <%= query.user === user._id.toString() ? 'selected' : '' %>>
<%= user.username %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">From Date</label>
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">To Date</label>
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Items per page</label>
<select class="form-select" name="limit">
<option value="5" <%= (query.limit && query.limit == '5') ? 'selected' : '' %>>5 items</option>
<option value="7" <%= (!query.limit || query.limit == '7') ? 'selected' : '' %>>7 items</option>
<option value="10" <%= (query.limit && query.limit == '10') ? 'selected' : '' %>>10 items</option>
<option value="15" <%= (query.limit && query.limit == '15') ? 'selected' : '' %>>15 items</option>
<option value="20" <%= (query.limit && query.limit == '20') ? 'selected' : '' %>>20 items</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-1">
<button type="submit" class="btn flex-fill" style="background-color: var(--primary-color); color: white;">
<i class="fas fa-search me-1"></i>Filter
</button>
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
<i class="fas fa-times"></i>
</a>
</div>
</div>
</form>
<div class="col-md-2">
<label class="form-label">User</label>
<select class="form-select" name="user">
<option value="">All Users</option>
<% users.forEach(u => { %>
<option value="<%= u._id %>" <%= query.user === u._id.toString() ? 'selected' : '' %>><%= u.username %></option>
<% }); %>
</select>
</div>
</div>
<div class="col-md-2">
<label class="form-label">From Date</label>
<input type="date" class="form-control" name="dateFrom" value="<%= query.dateFrom || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">To Date</label>
<input type="date" class="form-control" name="dateTo" value="<%= query.dateTo || '' %>">
</div>
<div class="col-md-1">
<label class="form-label">Per page</label>
<select class="form-select" name="limit" onchange="this.form.submit()">
<option value="8" <%= (!query.limit || query.limit == '8') ? 'selected' : '' %>>8</option>
<option value="15" <%= query.limit == '15' ? 'selected' : '' %>>15</option>
<option value="25" <%= query.limit == '25' ? 'selected' : '' %>>25</option>
</select>
</div>
<div class="col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
<a href="/admin/audit-logs" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
</div>
</div>
</form>
</div>
</div>
<!-- Audit Logs List -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<% if (auditLogs && auditLogs.length > 0) { %>
<!-- Summary Stats -->
<div class="row mb-4">
<div class="col-md-8">
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
of <%= pagination.totalCount %> audit logs
<% if (pagination.totalCount > pagination.limit) { %>
<span class="badge bg-info ms-2">
<%= pagination.total %> pages
</span>
<% } %>
</span>
</div>
<!-- Table -->
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-shield-alt"></i> Activity Log</h5>
<% if (pagination) { %>
<span class="badge badge-soft-primary"><%= pagination.totalCount %> records</span>
<% } %>
</div>
<div class="card-body p-0">
<% if (auditLogs && auditLogs.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:130px">Date / Time</th>
<th>Model</th>
<th>Action</th>
<th>User</th>
<th>Changes</th>
<th>IP Address</th>
<th style="width:80px">Details</th>
</tr>
</thead>
<tbody>
<% auditLogs.forEach(log => { %>
<tr>
<td>
<div style="font-size:0.8rem;font-weight:500"><%= new Date(log.createdAt).toLocaleDateString('en-GB') %></div>
<div style="font-size:0.75rem;color:var(--text-muted)"><%= new Date(log.createdAt).toLocaleTimeString() %></div>
</td>
<td><span class="badge badge-soft-primary"><%= log.model %></span></td>
<td>
<%
let badgeClass = 'badge-soft-primary';
if (log.action.includes('CREATE')) badgeClass = 'bg-soft-success';
else if (log.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
else if (log.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
%>
<span class="badge <%= badgeClass %>"><%= log.action %></span>
</td>
<td>
<% if (log.performedBy) { %>
<div style="font-weight:500;font-size:0.8125rem"><%= log.performedBy.username %></div>
<div style="font-size:0.75rem;color:var(--text-muted)"><%= log.performedBy.email %></div>
<% } else { %>
<span style="color:var(--text-muted);font-size:0.8125rem">System</span>
<% } %>
</td>
<td>
<% if (log.changes && log.changes.length > 0) { %>
<span class="badge badge-soft-accent"><%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %></span>
<div class="mt-1">
<% log.changes.slice(0, 2).forEach(change => { %>
<div style="font-size:0.75rem;color:var(--text-muted)"><%= change.field %></div>
<% }); %>
<% if (log.changes.length > 2) { %>
<div style="font-size:0.75rem;color:var(--text-muted)">+<%= log.changes.length - 2 %> more</div>
<% } %>
</div>
<div class="col-md-4">
<div class="d-flex justify-content-end">
<div class="btn-group btn-group-sm me-3" role="group">
<input type="radio" class="btn-check" name="viewMode" id="tableView" autocomplete="off" checked>
<label class="btn btn-outline-secondary" for="tableView">
<i class="fas fa-table"></i> Table
</label>
<input type="radio" class="btn-check" name="viewMode" id="cardView" autocomplete="off">
<label class="btn btn-outline-secondary" for="cardView">
<i class="fas fa-th-large"></i> Cards
</label>
</div>
</div>
</div>
</div>
<% } else { %>
<span style="color:var(--text-muted)">—</span>
<% } %>
</td>
<td style="font-size:0.8rem;color:var(--text-muted)"><%= log.ipAddress %></td>
<td>
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm btn-outline-primary btn-icon" title="View">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- Table View -->
<div id="tableViewContent" class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th scope="col" style="width: 140px; color: var(--primary-dark);">Date/Time</th>
<th scope="col" style="color: var(--primary-dark);">Model</th>
<th scope="col" style="color: var(--primary-dark);">Action</th>
<th scope="col" style="color: var(--primary-dark);">User</th>
<th scope="col" style="color: var(--primary-dark);">Changes</th>
<th scope="col" style="color: var(--primary-dark);">IP Address</th>
<th scope="col" style="width: 120px; color: var(--primary-dark);">Actions</th>
</tr>
</thead>
<tbody>
<% auditLogs.forEach((log, index) => { %>
<tr>
<td>
<small class="text-muted">
<%= new Date(log.createdAt).toLocaleDateString() %><br>
<%= new Date(log.createdAt).toLocaleTimeString() %>
</small>
</td>
<td>
<span class="badge" style="background-color: var(--primary-color); color: white;">
<%= log.model %>
</span>
</td>
<td>
<%
let actionStyle = 'background-color: var(--primary-color); color: white;';
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
%>
<span class="badge" style="<%= actionStyle %>">
<%= log.action %>
</span>
</td>
<td>
<% if (log.performedBy) { %>
<div>
<strong style="color: var(--primary-dark);"><%= log.performedBy.username %></strong><br>
<small class="text-muted"><%= log.performedBy.email %></small>
</div>
<% } else { %>
<span class="text-muted">System</span>
<% } %>
</td>
<td>
<% if (log.changes && log.changes.length > 0) { %>
<span class="badge" style="background-color: var(--primary-color); color: white;">
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
</span>
<div class="mt-1">
<% log.changes.slice(0, 3).forEach(change => { %>
<small class="text-muted d-block">
<strong><%= change.field %></strong>
</small>
<% }); %>
<% if (log.changes.length > 3) { %>
<small class="text-muted">
+<%= log.changes.length - 3 %> more...
</small>
<% } %>
</div>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<small class="text-muted">
<%= log.ipAddress %>
</small>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm" style="background-color: var(--primary-color); color: white;">
<i class="fas fa-eye me-1"></i>View
</a>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- Card View (Hidden by default) -->
<div id="cardViewContent" style="display: none;">
<% auditLogs.forEach((log, index) => { %>
<div class="audit-card">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center py-2">
<span class="badge" style="background-color: var(--primary-color); color: white; font-size: 0.7rem;"><%= log.model %></span>
<small class="text-muted" style="font-size: 0.7rem;">
<%= new Date(log.createdAt).toLocaleDateString() %>
</small>
</div>
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<%
let actionStyle = 'background-color: var(--primary-color); color: white;';
if (log.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
else if (log.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
else if (log.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
%>
<span class="badge" style="<%= actionStyle %>; font-size: 0.65rem;">
<%= log.action %>
</span>
<small class="text-muted" style="font-size: 0.7rem;">
<%= new Date(log.createdAt).toLocaleTimeString() %>
</small>
</div>
<div class="mb-2">
<strong style="font-size: 0.8rem;">User:</strong>
<% if (log.performedBy) { %>
<div style="color: var(--primary-dark); font-size: 0.8rem;"><%= log.performedBy.username %></div>
<small class="text-muted" style="font-size: 0.7rem;"><%= log.performedBy.email %></small>
<% } else { %>
<span class="text-muted" style="font-size: 0.8rem;">System</span>
<% } %>
</div>
<% if (log.changes && log.changes.length > 0) { %>
<div class="mb-2">
<strong style="font-size: 0.8rem;">Changes:</strong>
<span class="badge ms-1" style="background-color: var(--primary-color); color: white; font-size: 0.65rem;">
<%= log.changes.length %> field<%= log.changes.length > 1 ? 's' : '' %>
</span>
<div class="mt-1">
<% log.changes.slice(0, 2).forEach(change => { %>
<small class="text-muted d-block" style="font-size: 0.7rem;">
• <%= change.field %>
</small>
<% }); %>
<% if (log.changes.length > 2) { %>
<small class="text-muted" style="font-size: 0.7rem;">
+<%= log.changes.length - 2 %> more...
</small>
<% } %>
</div>
</div>
<% } %>
<div class="mb-2">
<small class="text-muted" style="font-size: 0.7rem;">
IP: <%= log.ipAddress %>
</small>
</div>
</div>
<div class="card-footer p-2">
<a href="/admin/audit-logs/<%= log._id %>" class="btn btn-sm w-100" style="background-color: var(--primary-color); color: white; font-size: 0.8rem;">
<i class="fas fa-eye me-1"></i>View Details
</a>
</div>
</div>
</div>
<% }); %>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<nav aria-label="Audit log pagination" class="mt-4">
<ul class="pagination justify-content-center">
<% if (pagination.current > 1) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Previous</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<% if (i === pagination.current) { %>
<li class="page-item active">
<span class="page-link" style="background-color: var(--primary-color); border-color: var(--primary-color);"><%= i %></span>
</li>
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">
<%= i %>
</a>
</li>
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<% } %>
<% } %>
<% if (pagination.current < pagination.total) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(key => key !== 'page' ? '&' + key + '=' + encodeURIComponent(query[key]) : '').join('') %>" style="color: var(--primary-dark);">Next</a>
</li>
<% } %>
</ul>
<!-- Pagination Info -->
<div class="text-center mt-2">
<small class="text-muted">
Page <%= pagination.current %> of <%= pagination.total %>
(showing <%= ((pagination.current - 1) * pagination.limit) + 1 %> -
<%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %>
of <%= pagination.totalCount %> total items)
</small>
</div>
</nav>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="d-flex align-items-center justify-content-between px-3 py-2" style="border-top:1px solid var(--border-light);">
<small style="color:var(--text-muted)">
Showing <%= ((pagination.current - 1) * pagination.limit) + 1 %><%= Math.min(pagination.current * pagination.limit, pagination.totalCount) %> of <%= pagination.totalCount %>
</small>
<nav>
<ul class="pagination pagination-sm mb-0" style="gap:2px;">
<% if (pagination.current > 1) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current - 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
<i class="fas fa-chevron-left" style="font-size:0.7rem"></i>
</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<% if (i === pagination.current) { %>
<li class="page-item active">
<span class="page-link" style="background:var(--primary-color);border-color:var(--primary-color)"><%= i %></span>
</li>
<% } else if (i === 1 || i === pagination.total || (i >= pagination.current - 2 && i <= pagination.current + 2)) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= i %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>"><%= i %></a>
</li>
<% } else if (i === pagination.current - 3 || i === pagination.current + 3) { %>
<li class="page-item disabled"><span class="page-link">…</span></li>
<% } %>
<% } else { %>
<div class="text-center py-5">
<i class="fas fa-clipboard-list text-muted mb-3" style="font-size: 3rem;"></i>
<h5 class="text-muted mb-3">No Audit Logs Found</h5>
<p class="text-muted">No audit logs match your current filters.</p>
<a href="/admin/audit-logs" class="btn" style="background-color: var(--primary-color); color: white;">
<i class="fas fa-refresh me-1"></i>Clear Filters
</a>
</div>
<% } %>
<% } %>
<% if (pagination.current < pagination.total) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current + 1 %><%= Object.keys(query).map(k => k !== 'page' ? '&' + k + '=' + encodeURIComponent(query[k]) : '').join('') %>">
<i class="fas fa-chevron-right" style="font-size:0.7rem"></i>
</a>
</li>
<% } %>
</ul>
</nav>
</div>
</div>
<% } %>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-shield-alt"></i></div>
<h5>No audit logs found</h5>
<p>No activity matches your current filters.</p>
<a href="/admin/audit-logs" class="btn btn-outline-primary">Clear Filters</a>
</div>
<% } %>
</div>
</div>
<!-- Custom Cleanup Modal -->
<div id="cleanupModal" class="custom-modal" style="display: none;">
<div class="custom-modal-overlay"></div>
<div class="custom-modal-content">
<div class="custom-modal-header">
<h5 class="custom-modal-title">
<i class="fas fa-broom me-2"></i>Cleanup Old Audit Logs
</h5>
<button type="button" class="custom-modal-close" onclick="closeCleanupModal()">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" action="/admin/audit-logs/cleanup">
<div class="custom-modal-body">
<p>Delete audit logs older than the specified number of days.</p>
<div class="mb-3">
<label for="days" class="form-label">Keep logs for (days):</label>
<input type="number" class="form-control" id="days" name="days" value="90" min="1" max="365" required>
<div class="form-text">Recommended: 90 days for compliance</div>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Warning:</strong> This action cannot be undone. Deleted audit logs will be permanently removed.
</div>
</div>
<div class="custom-modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCleanupModal()">Cancel</button>
<button type="submit" class="btn btn-warning">
<i class="fas fa-broom me-1"></i>Cleanup Logs
</button>
</div>
</form>
<!-- Cleanup Modal -->
<div id="cleanupModal" style="display:none;position:fixed;inset:0;z-index:1050;">
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.45);" onclick="closeCleanupModal()"></div>
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border-radius:var(--border-radius-lg);box-shadow:var(--shadow-lg);width:90%;max-width:460px;overflow:hidden;">
<div style="padding:1rem 1.25rem;background:var(--warning-soft);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;">
<span style="font-weight:600;color:var(--warning-color);display:flex;align-items:center;gap:0.5rem;">
<i class="fas fa-broom"></i> Cleanup Old Audit Logs
</span>
<button onclick="closeCleanupModal()" style="background:none;border:none;cursor:pointer;color:var(--warning-color);font-size:1.1rem;"><i class="fas fa-times"></i></button>
</div>
<form method="POST" action="/admin/audit-logs/cleanup">
<div style="padding:1.25rem;">
<p style="font-size:var(--font-size-sm);color:var(--text-muted);margin-bottom:1rem;">Delete audit logs older than the specified number of days.</p>
<div class="mb-3">
<label class="form-label">Keep logs for (days)</label>
<input type="number" class="form-control" name="days" value="90" min="1" max="365" required>
<div class="form-text">Recommended: 90 days for compliance</div>
</div>
<div class="alert d-flex gap-2 align-items-start" style="background:var(--warning-soft);border:1px solid rgba(217,119,6,0.2);border-radius:var(--border-radius-sm);padding:0.75rem;">
<i class="fas fa-exclamation-triangle" style="color:var(--warning-color);margin-top:2px;flex-shrink:0;"></i>
<span style="font-size:var(--font-size-sm);color:var(--warning-color);">This action cannot be undone. Deleted logs will be permanently removed.</span>
</div>
</div>
<div style="padding:0.875rem 1.25rem;border-top:1px solid var(--border-color);display:flex;gap:0.5rem;justify-content:flex-end;background:#fafbfc;">
<button type="button" class="btn btn-outline-secondary" onclick="closeCleanupModal()">Cancel</button>
<button type="submit" class="btn btn-warning"><i class="fas fa-broom"></i> Cleanup</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// View mode toggle
const tableViewRadio = document.getElementById('tableView');
const cardViewRadio = document.getElementById('cardView');
const tableViewContent = document.getElementById('tableViewContent');
const cardViewContent = document.getElementById('cardViewContent');
// Debug: Check if elements exist
console.log('Elements found:', {
tableViewRadio: !!tableViewRadio,
cardViewRadio: !!cardViewRadio,
tableViewContent: !!tableViewContent,
cardViewContent: !!cardViewContent
});
if (!tableViewRadio || !cardViewRadio || !tableViewContent || !cardViewContent) {
console.error('Some view toggle elements are missing!');
return;
}
tableViewRadio.addEventListener('change', function() {
if (this.checked) {
tableViewContent.classList.remove('hidden');
cardViewContent.classList.remove('active');
console.log('Table view activated');
}
});
cardViewRadio.addEventListener('change', function() {
if (this.checked) {
tableViewContent.classList.add('hidden');
cardViewContent.classList.add('active');
console.log('Card view activated with CSS Grid');
}
});
// Auto-submit form when changing items per page
const limitSelect = document.querySelector('select[name="limit"]');
if (limitSelect) {
limitSelect.addEventListener('change', function() {
// Reset to page 1 when changing limit
const pageInput = document.querySelector('input[name="page"]');
if (pageInput) {
pageInput.value = 1;
} else {
// Create hidden input for page
const hiddenPageInput = document.createElement('input');
hiddenPageInput.type = 'hidden';
hiddenPageInput.name = 'page';
hiddenPageInput.value = 1;
this.form.appendChild(hiddenPageInput);
}
// Submit the form
this.form.submit();
});
}
});
function openCleanupModal() {
document.getElementById('cleanupModal').style.display = 'block';
document.body.style.overflow = 'hidden';
}
function closeCleanupModal() {
document.getElementById('cleanupModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
// Close modal when clicking overlay
document.addEventListener('click', function(e) {
if (e.target.classList.contains('custom-modal-overlay')) {
closeCleanupModal();
}
});
// Close modal with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCleanupModal();
}
});
function exportLogs() {
// Get current filter parameters
const params = new URLSearchParams(window.location.search);
params.set('export', 'csv');
// Create download link
const exportUrl = '/admin/audit-logs/export?' + params.toString();
window.open(exportUrl, '_blank');
}
</script>
<style>
.badge {
font-size: 0.75em;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.table th {
border-top: none;
font-weight: 600;
color: var(--primary-dark);
}
.btn-group-sm .btn {
font-size: 0.75rem;
}
.pagination .page-link {
color: var(--primary-dark) !important;
background-color: #fff !important;
border: 1px solid #dee2e6 !important;
padding: 0.5rem 0.75rem;
text-decoration: none;
position: relative;
color: var(--primary-color);
border-color: var(--border-color);
border-radius: var(--border-radius-sm) !important;
font-size: 0.8125rem;
padding: 0.3rem 0.6rem;
}
.pagination .page-link:hover { background: var(--primary-soft); border-color: var(--primary-color); }
.pagination .page-item.active .page-link { color: #fff; }
.pagination .page-item.disabled .page-link { color: var(--text-muted); }
</style>
.pagination .page-item.active .page-link {
background-color: var(--primary-color) !important;
border-color: var(--primary-color) !important;
color: #fff !important;
z-index: 3;
font-weight: 600;
}
.pagination .page-link:hover {
color: var(--primary-color) !important;
background-color: #f8f9fa !important;
border-color: var(--primary-color) !important;
text-decoration: none;
}
.pagination .page-link:focus {
color: var(--primary-color) !important;
background-color: #f8f9fa !important;
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25);
outline: 0;
}
.pagination .page-item.disabled .page-link {
color: #6c757d !important;
background-color: #fff !important;
border-color: #dee2e6 !important;
}
/* Ensure pagination text is always visible */
.pagination .page-link span,
.pagination .page-link {
display: inline-block;
line-height: 1.25;
}
/* Custom Modal Styles */
.custom-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1050;
}
.custom-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.custom-modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
.custom-modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #dee2e6;
background-color: #fff3cd;
border-radius: 8px 8px 0 0;
display: flex;
justify-content: between;
align-items: center;
}
.custom-modal-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #856404;
flex: 1;
}
.custom-modal-close {
background: none;
border: none;
font-size: 1.2rem;
color: #856404;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.custom-modal-close:hover {
background-color: rgba(133, 100, 4, 0.1);
}
.custom-modal-body {
padding: 1.5rem;
}
.custom-modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
border-radius: 0 0 8px 8px;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
/* Card View Improvements - Using CSS Grid for better control */
#cardViewContent {
display: none !important;
visibility: hidden !important;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
padding: 0;
margin: 0;
}
#cardViewContent.active {
display: grid !important;
visibility: visible !important;
}
#cardViewContent .card {
transition: transform 0.2s, box-shadow 0.2s;
width: 100% !important;
margin: 0 !important;
}
#cardViewContent .card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important;
}
#cardViewContent .card-header {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6;
}
#cardViewContent .card-footer {
background-color: #f8f9fa !important;
border-top: 1px solid #dee2e6;
}
/* Table View */
#tableViewContent {
display: block;
}
#tableViewContent.hidden {
display: none !important;
}
/* Responsive grid adjustments */
@media (max-width: 1400px) {
#cardViewContent {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important;
}
}
@media (max-width: 1200px) {
#cardViewContent {
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
}
}
@media (max-width: 992px) {
#cardViewContent {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
}
}
@media (max-width: 768px) {
#cardViewContent {
grid-template-columns: 1fr !important;
gap: 1rem !important;
}
.custom-modal-content {
width: 95%;
margin: 1rem;
}
}
</style>
<script>
function openCleanupModal() { document.getElementById('cleanupModal').style.display = 'block'; }
function closeCleanupModal() { document.getElementById('cleanupModal').style.display = 'none'; }
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeCleanupModal(); });
</script>

View File

@@ -1,314 +1,161 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Detailed audit log information</p>
</div>
<div class="d-flex gap-2">
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Audit Logs
</a>
</div>
</div>
<div class="row">
<!-- Main Information -->
<div class="col-12">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header">
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-info-circle me-2"></i>Audit Information
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label fw-bold">Model:</label>
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.model %></span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Action:</label>
<%
let actionStyle = 'background-color: var(--primary-color); color: white;';
if (auditLog.action.includes('CREATE')) actionStyle = 'background-color: #28a745; color: white;';
else if (auditLog.action.includes('UPDATE')) actionStyle = 'background-color: #ffc107; color: #212529;';
else if (auditLog.action.includes('DELETE')) actionStyle = 'background-color: #dc3545; color: white;';
%>
<span class="badge ms-2" style="<%= actionStyle %>"><%= auditLog.action %></span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Document ID:</label>
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.documentId %></code>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label fw-bold">Date & Time:</label>
<div class="ms-2">
<%= new Date(auditLog.createdAt).toLocaleDateString() %>
<%= new Date(auditLog.createdAt).toLocaleTimeString() %>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">User:</label>
<div class="ms-2">
<% if (auditLog.performedBy) { %>
<strong style="color: var(--primary-dark);"><%= auditLog.performedBy.username %></strong><br>
<small class="text-muted"><%= auditLog.performedBy.email %></small>
<% } else { %>
<span class="text-muted">System</span>
<% } %>
</div>
</div>
<div class="mb-3">
<label class="form-label fw-bold">IP Address:</label>
<code class="ms-2" style="background-color: #f8f9fa; color: var(--primary-dark);"><%= auditLog.ipAddress %></code>
</div>
</div>
</div>
<% if (auditLog.userAgent) { %>
<div class="mb-3">
<label class="form-label fw-bold">User Agent:</label>
<div class="ms-2">
<small class="text-muted font-monospace"><%= auditLog.userAgent %></small>
</div>
</div>
<% } %>
</div>
</div>
<!-- Changes Details -->
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
<div class="card border-0 shadow-sm mb-4">
<div class="card-header">
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-edit me-2"></i>Field Changes
<span class="badge ms-2" style="background-color: var(--primary-color); color: white;"><%= auditLog.changes.length %></span>
</h5>
</div>
<div class="card-body">
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th style="width: 200px; color: var(--primary-dark);">Field</th>
<th style="color: var(--primary-dark);">Before</th>
<th style="color: var(--primary-dark);">After</th>
</tr>
</thead>
<tbody>
<% auditLog.changes.forEach((change, index) => { %>
<tr>
<td>
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
</td>
<td>
<div class="change-value before">
<% if (change.before === null || change.before === undefined) { %>
<span class="text-muted fst-italic">null</span>
<% } else if (typeof change.before === 'object') { %>
<pre class="mb-0"><%= JSON.stringify(change.before, null, 2) %></pre>
<% } else { %>
<%= change.before %>
<% } %>
</div>
</td>
<td>
<div class="change-value after">
<% if (change.after === null || change.after === undefined) { %>
<span class="text-muted fst-italic">null</span>
<% } else if (typeof change.after === 'object') { %>
<pre class="mb-0"><%= JSON.stringify(change.after, null, 2) %></pre>
<% } else { %>
<%= change.after %>
<% } %>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Summary View:</strong> Detailed field values are restricted to administrators.
</div>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th style="color: var(--primary-dark);">Field</th>
<th style="color: var(--primary-dark);">Status</th>
</tr>
</thead>
<tbody>
<% auditLog.changes.forEach((change, index) => { %>
<tr>
<td>
<strong style="color: var(--primary-dark);"><%= change.field %></strong>
</td>
<td>
<span class="badge bg-info">Modified</span>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } %>
</div>
</div>
<% } %>
<!-- Summary Stats -->
<div class="card border-0 shadow-sm">
<div class="card-header">
<h6 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-chart-bar me-2"></i>Summary
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="border-end">
<div class="h5 mb-0" style="color: var(--primary-color);">
<%= auditLog.changes ? auditLog.changes.length : 0 %>
</div>
<small class="text-muted">Fields Changed</small>
</div>
</div>
<div class="col-6">
<div class="h5 mb-0" style="color: var(--primary-color);">
<%= new Date(auditLog.createdAt).toLocaleDateString() === new Date().toLocaleDateString() ? 'Today' : 'Past' %>
</div>
<small class="text-muted">Timing</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Raw Data (Admin Only - Collapsible) -->
<!-- <% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
<div class="card border-0 shadow-sm mt-4">
<div class="card-header">
<h5 class="card-title mb-0">
<button class="btn btn-link p-0 text-decoration-none" type="button" data-bs-toggle="collapse" data-bs-target="#rawDataCollapse" aria-expanded="false" style="color: var(--primary-dark);">
<i class="fas fa-code me-2"></i>Raw Data <span class="badge bg-warning text-dark ms-2">Admin Only</span>
<i class="fas fa-chevron-down ms-2"></i>
</button>
</h5>
</div>
<div class="collapse" id="rawDataCollapse">
<div class="card-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Security Notice:</strong> Raw data contains sensitive information and is only visible to administrators.
</div>
<div class="row">
<% if (auditLog.before) { %>
<div class="col-md-6">
<h6 class="text-danger">Before:</h6>
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.before, null, 2) %></pre>
</div>
<% } %>
<% if (auditLog.after) { %>
<div class="col-md-6">
<h6 class="text-success">After:</h6>
<pre class="p-3 rounded" style="background-color: #f8f9fa; border: 1px solid #dee2e6; max-height: 400px; overflow-y: auto;"><%= JSON.stringify(auditLog.after, null, 2) %></pre>
</div>
<% } %>
</div>
<% if (!auditLog.before && !auditLog.after) { %>
<div class="text-center text-muted py-3">
<i class="fas fa-info-circle me-2"></i>No raw data available for this audit log.
</div>
<% } %>
</div>
</div>
</div>
<% } else { %>
<div class="card border-0 shadow-sm mt-4">
<div class="card-header">
<h5 class="card-title mb-0" style="color: var(--primary-dark);">
<i class="fas fa-lock me-2"></i>Raw Data
</h5>
</div>
<div class="card-body">
<div class="alert alert-warning">
<i class="fas fa-shield-alt me-2"></i>
<strong>Access Restricted:</strong> Raw data access is limited to administrators for security reasons.
</div>
<div class="text-center text-muted py-3">
<i class="fas fa-user-shield" style="font-size: 3rem; opacity: 0.3;"></i>
<p class="mt-3">Contact your administrator if you need access to detailed raw data.</p>
</div>
</div>
</div>
<% } %> -->
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Audit Log Details</h1>
<p class="subtitle">Detailed activity record</p>
</div>
<a href="/admin/audit-logs" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
<style>
.change-value {
max-width: 300px;
word-wrap: break-word;
overflow-wrap: break-word;
}
<div class="row g-3">
.change-value pre {
max-height: 200px;
overflow-y: auto;
font-size: 0.8rem;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
color: var(--primary-dark);
}
<!-- Main Info -->
<div class="col-lg-8">
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-info-circle"></i> Audit Information</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<div class="mb-3">
<div class="form-label">Model</div>
<span class="badge badge-soft-primary" style="font-size:0.8rem"><%= auditLog.model %></span>
</div>
<div class="mb-3">
<div class="form-label">Action</div>
<%
let badgeClass = 'badge-soft-primary';
if (auditLog.action.includes('CREATE')) badgeClass = 'bg-soft-success';
else if (auditLog.action.includes('UPDATE')) badgeClass = 'bg-soft-warning';
else if (auditLog.action.includes('DELETE')) badgeClass = 'bg-soft-danger';
%>
<span class="badge <%= badgeClass %>" style="font-size:0.8rem"><%= auditLog.action %></span>
</div>
<div class="mb-3">
<div class="form-label">Document ID</div>
<code style="font-size:0.8rem;color:var(--primary-color)"><%= auditLog.documentId %></code>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-label">Date & Time</div>
<div style="font-size:var(--font-size-sm)">
<%= new Date(auditLog.createdAt).toLocaleDateString('en-GB') %>
<span style="color:var(--text-muted)"><%= new Date(auditLog.createdAt).toLocaleTimeString() %></span>
</div>
</div>
<div class="mb-3">
<div class="form-label">Performed By</div>
<% if (auditLog.performedBy) { %>
<div style="font-weight:500;font-size:var(--font-size-sm)"><%= auditLog.performedBy.username %></div>
<div style="font-size:var(--font-size-xs);color:var(--text-muted)"><%= auditLog.performedBy.email %></div>
<% } else { %>
<span style="color:var(--text-muted);font-size:var(--font-size-sm)">System</span>
<% } %>
</div>
<div class="mb-3">
<div class="form-label">IP Address</div>
<code style="font-size:0.8rem;color:var(--text-muted)"><%= auditLog.ipAddress %></code>
</div>
</div>
<% if (auditLog.userAgent) { %>
<div class="col-12">
<div class="form-label">User Agent</div>
<div style="font-size:0.75rem;color:var(--text-muted);font-family:monospace;word-break:break-all"><%= auditLog.userAgent %></div>
</div>
<% } %>
</div>
</div>
</div>
.change-value.before {
background-color: #fff5f5;
border-left: 3px solid #dc3545;
padding-left: 8px;
}
<!-- Field Changes -->
<% if (auditLog.changes && auditLog.changes.length > 0) { %>
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-pen"></i> Field Changes</h5>
<span class="badge badge-soft-accent"><%= auditLog.changes.length %> field<%= auditLog.changes.length > 1 ? 's' : '' %></span>
</div>
<div class="card-body p-0">
<% if (user && (user.role === 'admin' || user.role === 'superadmin')) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:160px">Field</th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody>
<% auditLog.changes.forEach(change => { %>
<tr>
<td style="font-weight:500;font-size:var(--font-size-sm)"><%= change.field %></td>
<td>
<div style="background:var(--danger-soft);border-left:3px solid var(--danger-color);padding:0.4rem 0.6rem;border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0;font-size:0.8rem;word-break:break-all;">
<% if (change.before === null || change.before === undefined) { %>
<em style="color:var(--text-muted)">null</em>
<% } else if (typeof change.before === 'object') { %>
<pre style="margin:0;font-size:0.75rem;max-height:100px;overflow:auto"><%= JSON.stringify(change.before, null, 2) %></pre>
<% } else { %>
<%= change.before %>
<% } %>
</div>
</td>
<td>
<div style="background:var(--success-soft);border-left:3px solid var(--success-color);padding:0.4rem 0.6rem;border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0;font-size:0.8rem;word-break:break-all;">
<% if (change.after === null || change.after === undefined) { %>
<em style="color:var(--text-muted)">null</em>
<% } else if (typeof change.after === 'object') { %>
<pre style="margin:0;font-size:0.75rem;max-height:100px;overflow:auto"><%= JSON.stringify(change.after, null, 2) %></pre>
<% } else { %>
<%= change.after %>
<% } %>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="p-3">
<div class="alert d-flex gap-2 align-items-center" style="background:var(--info-soft);border:1px solid rgba(14,116,144,0.15);border-radius:var(--border-radius-sm);padding:0.75rem;">
<i class="fas fa-info-circle" style="color:var(--info-color)"></i>
<span style="font-size:var(--font-size-sm)">Detailed field values are restricted to administrators.</span>
</div>
<% auditLog.changes.forEach(change => { %>
<div style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0;border-bottom:1px solid var(--border-light);">
<span style="font-weight:500;font-size:var(--font-size-sm)"><%= change.field %></span>
<span class="badge bg-soft-info">Modified</span>
</div>
<% }); %>
</div>
<% } %>
</div>
</div>
<% } %>
</div>
.change-value.after {
background-color: #f0fff4;
border-left: 3px solid #28a745;
padding-left: 8px;
}
<!-- Sidebar summary -->
<div class="col-lg-4">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-chart-bar"></i> Summary</h5>
</div>
<div class="card-body d-flex flex-column gap-2">
<div class="stat-card primary">
<div class="stat-card-value"><%= auditLog.changes ? auditLog.changes.length : 0 %></div>
<div class="stat-card-label">Fields Changed</div>
</div>
<div class="stat-card accent">
<div class="stat-card-value" style="font-size:1.25rem"><%= new Date(auditLog.createdAt).toLocaleDateString('en-GB') === new Date().toLocaleDateString('en-GB') ? 'Today' : 'Past' %></div>
<div class="stat-card-label">Timing</div>
</div>
</div>
</div>
</div>
.font-monospace {
font-family: 'Courier New', Courier, monospace;
font-size: 0.8rem;
}
.btn-link {
color: var(--primary-dark) !important;
}
.btn-link:hover {
color: var(--primary-color) !important;
}
.table th {
border-top: none;
font-weight: 600;
color: var(--primary-dark);
}
.card-header {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6;
}
.badge {
font-size: 0.75em;
}
</style>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,300 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Manage blog posts and articles</p>
</div>
<div class="d-flex gap-2">
<% if (typeof frontendUrl !=='undefined' ) { %>
<a href="<%= frontendUrl %>/blog" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Blog Page
</a>
<% } %>
<a href="/admin/blog/create" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Create New Post
</a>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" action="/admin/blog" class="row g-3">
<div class="col-md-3">
<label class="form-label">Search</label>
<input type="text" class="form-control" name="search" value="<%= query.search || '' %>"
placeholder="Search title or excerpt...">
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="">All</option>
<option value="published" <%=query.status==='published' ? 'selected' : '' %>>Published</option>
<option value="draft" <%=query.status==='draft' ? 'selected' : '' %>>Draft</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Category</label>
<select class="form-select" name="category">
<option value="">All Categories</option>
<% categories.forEach(cat=> { %>
<option value="<%= cat.name %>" <%=query.category===cat.name ? 'selected' : '' %>>
<%= cat.name %>
</option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search me-1"></i>Filter
</button>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<a href="/admin/blog" class="btn btn-outline-secondary w-100">
<i class="fas fa-times me-1"></i>Clear
</a>
</div>
</form>
</div>
</div>
<!-- Blog List -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<% if (blogs && blogs.length> 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th scope="col" style="width: 60px">Image</th>
<th scope="col">Title</th>
<th scope="col">Category</th>
<th scope="col">Status</th>
<th scope="col">Author</th>
<th scope="col">Published</th>
<th scope="col" style="width: 200px">Actions</th>
</tr>
</thead>
<tbody>
<% blogs.forEach((blog, index)=> { %>
<tr>
<td>
<% if (blog.featuredImage) { %>
<img src="<%= getFullImageUrl(blog.featuredImage, backendUrl) %>"
alt="<%= blog.title %>" class="img-thumbnail"
style="width: 50px; height: 50px; object-fit: cover;">
<% } else { %>
<div class="bg-light d-flex align-items-center justify-content-center"
style="width: 50px; height: 50px;">
<i class="fas fa-image text-muted"></i>
</div>
<% } %>
</td>
<td>
<div>
<span class="fw-medium">
<%= blog.title %>
</span>
<% if (blog.isFeatured) { %>
<span class="badge bg-warning text-dark ms-2">Featured</span>
<% } %>
</div>
<small class="text-muted">
<%= blog.excerpt.substring(0, 60) %>...
</small>
</td>
<td>
<% if (blog.category && blog.category.length> 0) { %>
<% blog.category.slice(0, 2).forEach(cat=> { %>
<span class="badge bg-secondary me-1">
<%= cat %>
</span>
<% }); %>
<% if (blog.category.length> 2) { %>
<span class="text-muted">+<%= blog.category.length - 2 %></span>
<% } %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (blog.status==='published' ) { %>
<span class="badge bg-success">Published</span>
<% } else { %>
<span class="badge bg-secondary">Draft</span>
<% } %>
</td>
<td>
<%= blog.author || 'Admin' %>
</td>
<td>
<%= blog.publishedAt || '-' %>
</td>
<td>
<div class="btn-group" role="group">
<% if (typeof frontendUrl !=='undefined' ) { %>
<a href="<%= frontendUrl %>/blog/<%= blog.slug %>" target="_blank"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
<% } %>
<a href="/admin/blog/<%= blog._id %>/edit"
class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit me-1"></i>Edit
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-custom-modal="open" data-id="<%= blog._id %>"
data-title="<%= blog.title %>">
<i class="fas fa-trash-alt me-1"></i>Delete
</button>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total> 1) { %>
<nav aria-label="Blog pagination" class="mt-4">
<ul class="pagination justify-content-center">
<% if (pagination.current> 1) { %>
<li class="page-item">
<a class="page-link"
href="?page=<%= pagination.current - 1 %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">Previous</a>
</li>
<% } %>
<% for (let i=1; i <=pagination.total; i++) { %>
<% if (i===pagination.current) { %>
<li class="page-item active">
<span class="page-link">
<%= i %>
</span>
</li>
<% } else if (i===1 || i===pagination.total || (i>= pagination.current - 2
&& i <= pagination.current + 2)) { %>
<li class="page-item">
<a class="page-link"
href="?page=<%= i %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">
<%= i %>
</a>
</li>
<% } else if (i===pagination.current - 3 || i===pagination.current +
3) { %>
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
<% } %>
<% } %>
<% if (pagination.current < pagination.total) { %>
<li class="page-item">
<a class="page-link"
href="?page=<%= pagination.current + 1 %><%= query.search ? '&search=' + query.search : '' %><%= query.status ? '&status=' + query.status : '' %><%= query.category ? '&category=' + query.category : '' %>">Next</a>
</li>
<% } %>
</ul>
</nav>
<% } %>
<% } else { %>
<div class="text-center py-5">
<i class="fas fa-blog text-muted mb-3" style="font-size: 3rem;"></i>
<h5 class="text-muted mb-3">No Blog Posts Found</h5>
<a href="/admin/blog/create" class="btn btn-primary">
<i class="fas fa-plus-circle me-1"></i>Create First Blog Post
</a>
</div>
<% } %>
</div>
</div>
</div>
<!-- Delete Blog Confirmation Modal -->
<div class="modal fade" id="deleteBlogModal" tabindex="-1" aria-labelledby="deleteBlogModalLabel" aria-hidden="true"
data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteBlogModalLabel">
<i class="fas fa-trash me-2"></i>Confirm Delete
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the blog post "<span id="deleteBlogTitle" class="fw-bold"></span>"?
</p>
<p class="text-danger mb-0">
<small>
<i class="fas fa-exclamation-triangle me-1"></i>This action cannot be
undone.</small>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form id="deleteBlogForm" method="POST" style="display: inline;">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-1"></i>Delete
</button>
</form>
</div>
</div>
</div>
</div>
<style>
/* Fix modal z-index - must be higher than everything else */
#deleteBlogModal {
z-index: 2050 !important;
}
#deleteBlogModal .modal-dialog {
z-index: 2060 !important;
position: relative;
}
#deleteBlogModal .modal-content {
z-index: 2070 !important;
position: relative;
}
/* Ensure modal is clickable */
#deleteBlogModal.show {
display: block !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () { // Initialize modal instance once
const deleteModalElement = document.getElementById('deleteBlogModal');
const deleteModal = new bootstrap.Modal(deleteModalElement, {
backdrop: false,
keyboard: true,
focus: true
});
// Handle delete buttons
document.querySelectorAll('[data-custom-modal="open"]').forEach(button => {
button.addEventListener('click', function (e) {
e.preventDefault();
const blogId = this.getAttribute('data-id');
const blogTitle = this.getAttribute('data-title');
// Set blog title in modal
document.getElementById('deleteBlogTitle').textContent = blogTitle;
// Set form action
document.getElementById('deleteBlogForm').action = `/admin/blog/${blogId}/delete`;
// Show Bootstrap modal
deleteModal.show();
});
});
});
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
<div class="page-title-area">
<div>
<h1>Create Certificate</h1>
<p class="subtitle">Register a new certificate record</p>
</div>
<a href="/admin/certificate" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
<i class="fas fa-exclamation-circle"></i> <%= error %>
</div>
<% } %>
<form method="POST" action="/admin/certificate/create" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Certificate No. <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="certification_number" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.certification_number || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="student_name" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.student_name || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Program / Title <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="program_name" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.program_name || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Passport No.</label>
<input type="text" class="form-control" name="passport_number"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.passport_number || '' : '' %>">
</div>
<div class="col-12">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.address || '' : '' %>">
</div>
</div>
</div>
</div>
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Department <span class="text-danger">*</span></label>
<select class="form-select" name="department" required>
<option value="">-- Select --</option>
<% departments.forEach(d => { %>
<option value="<%= d._id %>" <%= (typeof formData !== 'undefined' && formData && formData.department === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Level <span class="text-danger">*</span></label>
<select class="form-select" name="level" required>
<option value="">-- Select --</option>
<% levels.forEach(l => { %>
<option value="<%= l._id %>" <%= (typeof formData !== 'undefined' && formData && formData.level === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="issued_date" required
value="<%= (typeof formData !== 'undefined' && formData && formData.issued_date) ? formData.issued_date.toString().substring(0,10) : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="active">Active</option>
<option value="revoked" <%= (typeof formData !== 'undefined' && formData && formData.status === 'revoked') ? 'selected' : '' %>>Revoked</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Certificate Image</h5></div>
<div class="card-body">
<input type="file" class="form-control" name="certificate_image" accept="image/*">
</div>
</div>
<div class="card border-0">
<div class="card-body d-flex flex-column gap-2">
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Create Certificate</button>
<a href="/admin/certificate" class="btn btn-outline-secondary w-100">Cancel</a>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,107 @@
<div class="page-title-area">
<div>
<h1>Edit Certificate</h1>
<p class="subtitle"><code style="font-size:0.8rem;color:var(--info-color)"><%= cert.certification_number %></code></p>
</div>
<a href="/admin/certificate" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
<i class="fas fa-exclamation-circle"></i> <%= error %>
</div>
<% } %>
<form method="POST" action="/admin/certificate/<%= cert._id %>/edit" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Certificate No. <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="certification_number" required value="<%= cert.certification_number %>">
</div>
<div class="col-md-6">
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="student_name" required value="<%= cert.student_name %>">
</div>
<div class="col-md-6">
<label class="form-label">Program / Title <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="program_name" required value="<%= cert.program_name %>">
</div>
<div class="col-md-6">
<label class="form-label">Passport No.</label>
<input type="text" class="form-control" name="passport_number" value="<%= cert.passport_number || '' %>">
</div>
<div class="col-12">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address" value="<%= cert.address || '' %>">
</div>
</div>
</div>
</div>
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Department <span class="text-danger">*</span></label>
<select class="form-select" name="department" required>
<option value="">-- Select --</option>
<% departments.forEach(d => { %>
<option value="<%= d._id %>" <%= (cert.department && cert.department._id && cert.department._id.toString() === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Level <span class="text-danger">*</span></label>
<select class="form-select" name="level" required>
<option value="">-- Select --</option>
<% levels.forEach(l => { %>
<option value="<%= l._id %>" <%= (cert.level && cert.level._id && cert.level._id.toString() === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="issued_date" required value="<%= cert.issued_date ? new Date(cert.issued_date).toISOString().substring(0,10) : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="active" <%= cert.status === 'active' ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= cert.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Certificate Image</h5></div>
<div class="card-body">
<% if (cert.certificate_image) { %>
<div class="mb-2">
<img src="/admin/files/<%= cert.certificate_image %>" alt="Certificate image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
</div>
<% } %>
<input type="file" class="form-control" name="certificate_image" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
</div>
</div>
<div class="card border-0">
<div class="card-body d-flex flex-column gap-2">
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Save Changes</button>
<a href="/admin/certificate" class="btn btn-outline-secondary w-100">Cancel</a>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,102 @@
<div class="page-title-area">
<div>
<h1>Certificates</h1>
<p class="subtitle">Certificate records</p>
</div>
<a href="/admin/certificate/create" class="btn btn-primary">
<i class="fas fa-plus"></i> New Certificate
</a>
</div>
<!-- Filter -->
<div class="card border-0 mb-3">
<div class="card-body" style="padding:1rem 1.25rem;">
<form method="GET" action="/admin/certificate">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<div class="position-relative search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" class="form-control" name="search" placeholder="Search name, number..." value="<%= query.search || '' %>">
</div>
</div>
<div class="col-md-2">
<select class="form-select" name="status">
<option value="">All Status</option>
<option value="active" <%= query.status === 'active' ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= query.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
</select>
</div>
<div class="col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
<% if (query.search || query.status) { %>
<a href="/admin/certificate" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
<% } %>
</div>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="card border-0">
<div class="card-body p-0">
<% if (certificates && certificates.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:40px">#</th>
<th>Certificate No.</th>
<th>Full Name</th>
<th>Program</th>
<th>Department</th>
<th>Level</th>
<th>Issue Date</th>
<th>Status</th>
<th style="width:90px">Actions</th>
</tr>
</thead>
<tbody>
<% certificates.forEach((c, i) => { %>
<tr>
<td style="color:var(--text-muted)"><%= i + 1 %></td>
<td><code style="font-size:0.8rem;color:var(--info-color)"><%= c.certification_number %></code></td>
<td style="font-weight:500"><%= c.student_name %></td>
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= c.program_name %></td>
<td style="font-size:0.8125rem"><%= c.department ? c.department.name : '—' %></td>
<td style="font-size:0.8125rem"><%= c.level ? c.level.type : '—' %></td>
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= c.issued_date ? new Date(c.issued_date).toLocaleDateString('en-GB') : '—' %></td>
<td>
<% if (c.status === 'active') { %>
<span class="badge bg-soft-success">Active</span>
<% } else { %>
<span class="badge bg-soft-danger">Revoked</span>
<% } %>
</td>
<td>
<div class="table-actions">
<a href="/admin/certificate/<%= c._id %>/edit" class="btn btn-sm btn-outline-primary btn-icon" title="Edit">
<i class="fas fa-pen"></i>
</a>
<form method="POST" action="/admin/certificate/<%= c._id %>/delete" style="display:inline;" onsubmit="return confirm('Delete this certificate?')">
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-certificate"></i></div>
<h5>No certificates found</h5>
<p>Create the first certificate record.</p>
<a href="/admin/certificate/create" class="btn btn-primary"><i class="fas fa-plus"></i> Create</a>
</div>
<% } %>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,456 +1,155 @@
<div class="container mt-4 mb-4">
<h1 class="page-title mb-4">Dashboard</h1>
<!-- Page Header -->
<div class="page-title-area mb-4">
<div>
<h1 style="font-size:1.4rem;font-weight:700;color:var(--primary-color);margin:0;">Dashboard</h1>
<p class="text-muted mb-0" style="font-size:0.8125rem;">ULDP Management System overview</p>
</div>
<div class="d-flex gap-2">
<a href="/admin/qualification/create" class="btn btn-primary">
<i class="fas fa-graduation-cap"></i> New Qualification
</a>
<a href="/admin/certificate/create" class="btn btn-outline-primary">
<i class="fas fa-certificate"></i> New Certificate
</a>
</div>
</div>
<!-- Quick Actions -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" style="color: var(--primary-color)">Quick Management</h5>
<!-- Stats Grid -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-4 col-xl-2">
<div class="stat-card primary">
<div class="stat-card-value"><%= total %></div>
<div class="stat-card-label">Total Records</div>
</div>
<div class="card-body p-0">
<div class="row g-0">
<!-- Home -->
<div class="col-md-4 border-end">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-home fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Home</h5>
<p class="text-muted mb-0 small">Manage homepage</p>
</div>
</div>
<a href="/admin/home" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl-2">
<div class="stat-card accent">
<div class="stat-card-value"><%= qualificationCount %></div>
<div class="stat-card-label">Qualifications</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl-2">
<div class="stat-card info">
<div class="stat-card-value"><%= certificationCount %></div>
<div class="stat-card-label">Certificates</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl-2">
<div class="stat-card success">
<div class="stat-card-value"><%= activeCount %></div>
<div class="stat-card-label">Active</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl-2">
<div class="stat-card danger">
<div class="stat-card-value"><%= revokedCount %></div>
<div class="stat-card-label">Revoked</div>
</div>
</div>
<div class="col-6 col-md-4 col-xl-2">
<div class="stat-card warning">
<div class="stat-card-value"><%= recentCount %></div>
<div class="stat-card-label">New (30d)</div>
</div>
</div>
</div>
<!-- Header & Menu -->
<div class="col-md-4 border-end">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-bars fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Header & Menu</h5>
<p class="text-muted mb-0 small">Manage header & menu</p>
</div>
</div>
<a href="/admin/header" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
<div class="row g-3">
<!-- Recent Qualifications -->
<div class="col-lg-6">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-graduation-cap"></i> Latest Qualifications</h5>
<a href="/admin/qualification" class="btn btn-sm btn-outline-primary">View All</a>
</div>
<div class="card-body p-0">
<% if (recentQualifications && recentQualifications.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Qual. No.</th>
<th>Full Name</th>
<th>Type</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% recentQualifications.forEach(q => { %>
<tr>
<td><code style="font-size:0.8rem;color:var(--primary-color)"><%= q.qualification_number %></code></td>
<td style="font-weight:500;font-size:0.8125rem"><%= q.student_name %></td>
<td>
<% if (q.topic_name) { %>
<span class="badge badge-soft-primary">PhD</span>
<% } else { %>
<span class="badge bg-soft-info">MBA/Master</span>
<% } %>
</td>
<td>
<span class="badge <%= q.status === 'active' ? 'bg-soft-success' : 'bg-soft-danger' %>">
<%= q.status %>
</span>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<!-- Footer -->
<div class="col-md-4">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-window-minimize fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Footer</h5>
<p class="text-muted mb-0 small">Manage footer</p>
</div>
</div>
<a href="/admin/footer" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
<% } else { %>
<div class="empty-state" style="padding:2rem 1rem;">
<div class="empty-state-icon"><i class="fas fa-graduation-cap"></i></div>
<h5>No qualifications yet</h5>
<a href="/admin/qualification/create" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> Create</a>
</div>
</div>
<!-- About Us -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-users fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">About Us</h5>
<p class="text-muted mb-0 small">Manage about us</p>
</div>
</div>
<a href="/admin/about-us" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Contact -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-envelope fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Contact</h5>
<p class="text-muted mb-0 small">Manage contact</p>
</div>
</div>
<a href="/admin/contact" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Appointment -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-calendar-check fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Appointment</h5>
<p class="text-muted mb-0 small">Manage appointment page</p>
</div>
</div>
<a href="/admin/appointment" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Pricing -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-tags fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Pricing</h5>
<p class="text-muted mb-0 small">Manage pricing page</p>
</div>
</div>
<a href="/admin/pricing" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Services -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-concierge-bell fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Services</h5>
<p class="text-muted mb-0 small">Manage services</p>
</div>
</div>
<a href="/admin/service" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Blog -->
<div class="col-md-4 border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-blog fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Blog</h5>
<p class="text-muted mb-0 small">Manage blog posts</p>
</div>
</div>
<a href="/admin/blog" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<!-- Visa -->
<div class="col-md-4 border-end border-top">
<div class="p-4">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-passport fa-lg" style="color: var(--primary-color);"></i>
</div>
<div>
<h5 class="mb-0">Visa</h5>
<p class="text-muted mb-0 small">Manage visa countries</p>
</div>
</div>
<a href="/admin/visa" class="btn btn-sm btn-primary w-100 mt-2">
<i class="fas fa-edit me-2"></i>Edit
</a>
</div>
</div>
<% } %>
</div>
</div>
</div>
<!-- API Links Section -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">API Endpoints</h5>
<span class="badge bg-primary">6 APIs</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>API Name</th>
<th>Endpoint</th>
<th>Method</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-bars" style="color: var(--primary-color);"></i>
</div>
<span>Menu Header API</span>
</div>
</td>
<td><code>/api/header</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get menu header data</td>
<td>
<a href="/api/header" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-home" style="color: var(--primary-color);"></i>
</div>
<span>Home API</span>
</div>
</td>
<td><code>/api/home</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get homepage data</td>
<td>
<a href="/api/home" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
</div>
<span>About API</span>
</div>
</td>
<td><code>/api/about</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get about page data</td>
<td>
<a href="/api/about" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-users" style="color: var(--primary-color);"></i>
</div>
<span>About Us API</span>
</div>
</td>
<td><code>/api/about-us</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get about us data</td>
<td>
<a href="/api/about-us" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-envelope" style="color: var(--primary-color);"></i>
</div>
<span>Contact API</span>
</div>
</td>
<td><code>/api/contact</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get contact data</td>
<td>
<a href="/api/contact" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-blog" style="color: var(--primary-color);"></i>
</div>
<span>Blog API</span>
</div>
</td>
<td><code>/api/blog</code></td>
<td>
<span class="badge" style="background-color: var(--primary-color)">GET</span>
</td>
<td>API to get blog posts</td>
<td>
<a href="/api/blog" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-1"></i>View
</a>
</td>
</tr>
</tbody>
</table>
<!-- Recent Certificates -->
<div class="col-lg-6">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-certificate"></i> Latest Certificates</h5>
<a href="/admin/certificate" class="btn btn-sm btn-outline-primary">View All</a>
</div>
</div>
</div>
<!-- System Info -->
<div class="card">
<div class="card-header" style="
background: #0a2347;
color: white;
">
<h5 class="mb-0">System Information</h5>
</div>
<div class="card-body" style="background-color: #f8faf8">
<div class="row">
<div class="col-md-6">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
</div>
<div class="card-body p-0">
<% if (recentCertificates && recentCertificates.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Cert. No.</th>
<th>Full Name</th>
<th>Program</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% recentCertificates.forEach(c => { %>
<tr>
<td><code style="font-size:0.8rem;color:var(--info-color)"><%= c.certification_number %></code></td>
<td style="font-weight:500;font-size:0.8125rem"><%= c.student_name %></td>
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= c.program_name %></td>
<td>
<span class="badge <%= c.status === 'active' ? 'bg-soft-success' : 'bg-soft-danger' %>">
<%= c.status %>
</span>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<div>
<div class="text-muted small">Version</div>
<div class="fw-bold" style="color: var(--primary-color)">
CMS.HAILearning v1.0.0
</div>
<% } else { %>
<div class="empty-state" style="padding:2rem 1rem;">
<div class="empty-state-icon"><i class="fas fa-certificate"></i></div>
<h5>No certificates yet</h5>
<a href="/admin/certificate/create" class="btn btn-primary btn-sm"><i class="fas fa-plus"></i> Create</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-3">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-user" style="color: var(--primary-color);"></i>
</div>
<div>
<div class="text-muted small">Logged in as</div>
<div class="fw-bold" style="color: var(--primary-color);">
<%= user.username %>
</div>
</div>
</div>
</div>
</div>
<div class="alert mt-3 mb-0"
style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
<div class="d-flex">
<div class="me-3">
<i class="fas fa-lightbulb fa-lg" style="color: var(--primary-color)"></i>
</div>
<div>
<h6 class="mb-1" style="color: var(--primary-color)">Quick Tip</h6>
<p class="mb-0 text-muted">
Click the Edit button to make changes to your data.
</p>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<style>
.page-title {
color: var(--primary-color);
font-weight: 600;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
color: white;
}
.btn-outline-primary {
color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-outline-primary:hover {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
}
.card-header h5 {
color: white;
}
.badge {
background-color: var(--primary-color) !important;
color: white;
}
</style>

View File

@@ -0,0 +1,166 @@
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Create New Degree</h1>
<p class="subtitle">Fill in the details below to register a new degree</p>
</div>
<a href="/admin/degree" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert alert-danger d-flex align-items-center gap-2 mb-3" style="border-radius:var(--border-radius-sm);border:none;background:var(--danger-soft);color:var(--danger-color);">
<i class="fas fa-exclamation-circle"></i> <%= error %>
</div>
<% } %>
<form method="POST" action="/admin/degree/create" enctype="multipart/form-data">
<div class="row g-3">
<!-- Left column -->
<div class="col-lg-8">
<!-- Basic Info -->
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="qualification_number" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.qualification_number || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Certification No.</label>
<input type="text" class="form-control" name="certification_number"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.certification_number || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="student_name" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.student_name || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Program Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="program_name" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.program_name || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Passport No.</label>
<input type="text" class="form-control" name="passport_number"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.passport_number || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.address || '' : '' %>">
</div>
</div>
</div>
</div>
<!-- Classification -->
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Type <span class="text-danger">*</span></label>
<select class="form-select" name="type" required>
<option value="">-- Select --</option>
<option value="qualification" <%= (typeof formData !== 'undefined' && formData && formData.type === 'qualification') ? 'selected' : '' %>>Qualification</option>
<option value="certification" <%= (typeof formData !== 'undefined' && formData && formData.type === 'certification') ? 'selected' : '' %>>Certification</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Department <span class="text-danger">*</span></label>
<select class="form-select" name="department" required>
<option value="">-- Select --</option>
<% departments.forEach(dept => { %>
<option value="<%= dept._id %>" <%= (typeof formData !== 'undefined' && formData && formData.department === dept._id.toString()) ? 'selected' : '' %>><%= dept.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Level <span class="text-danger">*</span></label>
<select class="form-select" name="level" required>
<option value="">-- Select --</option>
<% levels.forEach(lvl => { %>
<option value="<%= lvl._id %>" <%= (typeof formData !== 'undefined' && formData && formData.level === lvl._id.toString()) ? 'selected' : '' %>><%= lvl.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="issued_date" required
value="<%= (typeof formData !== 'undefined' && formData && formData.issued_date) ? formData.issued_date.toString().substring(0,10) : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="active" <%= (typeof formData === 'undefined' || !formData || formData.status !== 'revoked') ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= (typeof formData !== 'undefined' && formData && formData.status === 'revoked') ? 'selected' : '' %>>Revoked</option>
</select>
</div>
</div>
</div>
</div>
<!-- Thesis -->
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Topic Title</label>
<input type="text" class="form-control" name="topic_name"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.topic_name || '' : '' %>">
</div>
<div class="col-12">
<label class="form-label">Short Description</label>
<textarea class="form-control" name="topic_short_desc" rows="3"><%= (typeof formData !== 'undefined' && formData) ? formData.topic_short_desc || '' : '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Right column: Images -->
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-images"></i> Images</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Degree Image</label>
<input type="file" class="form-control" name="degree_image" accept="image/*">
</div>
<div>
<label class="form-label">Certificate Image</label>
<input type="file" class="form-control" name="certificate_image" accept="image/*">
</div>
</div>
</div>
<!-- Actions -->
<div class="card border-0">
<div class="card-body d-flex flex-column gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-save"></i> Create Degree
</button>
<a href="/admin/degree" class="btn btn-outline-secondary w-100">Cancel</a>
</div>
</div>
</div>
</div>
</form>

170
views/admin/degree/edit.ejs Normal file
View File

@@ -0,0 +1,170 @@
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Edit Degree</h1>
<p class="subtitle"><code style="font-size:0.8rem;color:var(--accent-color)"><%= degree.qualification_number %></code></p>
</div>
<a href="/admin/degree" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert d-flex align-items-center gap-2 mb-3" style="border-radius:var(--border-radius-sm);border:none;background:var(--danger-soft);color:var(--danger-color);">
<i class="fas fa-exclamation-circle"></i> <%= error %>
</div>
<% } %>
<form method="POST" action="/admin/degree/<%= degree._id %>/edit" enctype="multipart/form-data">
<div class="row g-3">
<!-- Left column -->
<div class="col-lg-8">
<!-- Basic Info -->
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="qualification_number" required value="<%= degree.qualification_number %>">
</div>
<div class="col-md-6">
<label class="form-label">Certification No.</label>
<input type="text" class="form-control" name="certification_number" value="<%= degree.certification_number || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="student_name" required value="<%= degree.student_name %>">
</div>
<div class="col-md-6">
<label class="form-label">Program Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="program_name" required value="<%= degree.program_name %>">
</div>
<div class="col-md-6">
<label class="form-label">Passport No.</label>
<input type="text" class="form-control" name="passport_number" value="<%= degree.passport_number || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address" value="<%= degree.address || '' %>">
</div>
</div>
</div>
</div>
<!-- Classification -->
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Type <span class="text-danger">*</span></label>
<select class="form-select" name="type" required>
<option value="qualification" <%= degree.type === 'qualification' ? 'selected' : '' %>>Qualification</option>
<option value="certification" <%= degree.type === 'certification' ? 'selected' : '' %>>Certification</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Department <span class="text-danger">*</span></label>
<select class="form-select" name="department" required>
<option value="">-- Select --</option>
<% departments.forEach(dept => { %>
<option value="<%= dept._id %>" <%= (degree.department && degree.department._id && degree.department._id.toString() === dept._id.toString()) ? 'selected' : '' %>><%= dept.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Level <span class="text-danger">*</span></label>
<select class="form-select" name="level" required>
<option value="">-- Select --</option>
<% levels.forEach(lvl => { %>
<option value="<%= lvl._id %>" <%= (degree.level && degree.level._id && degree.level._id.toString() === lvl._id.toString()) ? 'selected' : '' %>><%= lvl.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="issued_date" required
value="<%= degree.issued_date ? new Date(degree.issued_date).toISOString().substring(0,10) : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="active" <%= degree.status === 'active' ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= degree.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
</select>
</div>
</div>
</div>
</div>
<!-- Thesis -->
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Topic Title</label>
<input type="text" class="form-control" name="topic_name" value="<%= degree.topic_name || '' %>">
</div>
<div class="col-12">
<label class="form-label">Short Description</label>
<textarea class="form-control" name="topic_short_desc" rows="3"><%= degree.topic_short_desc || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Right column -->
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-images"></i> Images</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Degree Image</label>
<% if (degree.degree_image) { %>
<div class="mb-2">
<img src="/admin/files/<%= degree.degree_image %>" alt="Degree image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
</div>
<% } %>
<input type="file" class="form-control" name="degree_image" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
</div>
<div>
<label class="form-label">Certificate Image</label>
<% if (degree.certificate_image) { %>
<div class="mb-2">
<img src="/admin/files/<%= degree.certificate_image %>" alt="Certificate image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
</div>
<% } %>
<input type="file" class="form-control" name="certificate_image" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="card border-0">
<div class="card-body d-flex flex-column gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-save"></i> Save Changes
</button>
<a href="/admin/degree" class="btn btn-outline-secondary w-100">Cancel</a>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,142 @@
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Degree Management</h1>
<p class="subtitle">All degrees registered in the system</p>
</div>
<a href="/admin/degree/create" class="btn btn-primary">
<i class="fas fa-plus"></i> New Degree
</a>
</div>
<!-- Search & Filter -->
<div class="card border-0 mb-3">
<div class="card-body" style="padding:1rem 1.25rem;">
<form method="GET" action="/admin/degree">
<div class="row g-2 align-items-end">
<div class="col-md-3">
<div class="position-relative search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" class="form-control" name="search" placeholder="Search name, number..." value="<%= query.search || '' %>">
</div>
</div>
<div class="col-md-2">
<select class="form-select" name="type">
<option value="">All Types</option>
<option value="qualification" <%= query.type === 'qualification' ? 'selected' : '' %>>Qualification</option>
<option value="certification" <%= query.type === 'certification' ? 'selected' : '' %>>Certification</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" name="department">
<option value="">All Departments</option>
<% departments.forEach(dept => { %>
<option value="<%= dept._id %>" <%= query.department === dept._id.toString() ? 'selected' : '' %>><%= dept.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<select class="form-select" name="level">
<option value="">All Levels</option>
<% levels.forEach(lvl => { %>
<option value="<%= lvl._id %>" <%= query.level === lvl._id.toString() ? 'selected' : '' %>><%= lvl.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-2">
<select class="form-select" name="status">
<option value="">All Status</option>
<option value="active" <%= query.status === 'active' ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= query.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
</select>
</div>
<div class="col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-primary flex-fill">
<i class="fas fa-search"></i>
</button>
<% if (query.search || query.type || query.department || query.level || query.status) { %>
<a href="/admin/degree" class="btn btn-outline-secondary">
<i class="fas fa-times"></i>
</a>
<% } %>
</div>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="card border-0">
<div class="card-body p-0">
<% if (degrees && degrees.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:40px">#</th>
<th>Qual. No.</th>
<th>Cert. No.</th>
<th>Full Name</th>
<th>Program</th>
<th>Type</th>
<th>Department</th>
<th>Level</th>
<th>Issue Date</th>
<th>Status</th>
<th style="width:90px">Actions</th>
</tr>
</thead>
<tbody>
<% degrees.forEach((degree, index) => { %>
<tr>
<td style="color:var(--text-muted)"><%= index + 1 %></td>
<td><code style="font-size:0.8rem;color:var(--primary-color)"><%= degree.qualification_number %></code></td>
<td><code style="font-size:0.8rem;color:var(--text-muted)"><%= degree.certification_number || '—' %></code></td>
<td style="font-weight:500"><%= degree.student_name %></td>
<td style="color:var(--text-muted);font-size:0.8125rem"><%= degree.program_name %></td>
<td>
<% if (degree.type === 'qualification') { %>
<span class="badge badge-soft-primary">Qualification</span>
<% } else { %>
<span class="badge bg-soft-info">Certification</span>
<% } %>
</td>
<td style="font-size:0.8125rem"><%= degree.department ? degree.department.name : '—' %></td>
<td style="font-size:0.8125rem"><%= degree.level ? degree.level.type : '—' %></td>
<td style="color:var(--text-muted);font-size:0.8125rem"><%= degree.issued_date ? new Date(degree.issued_date).toLocaleDateString('en-GB') : '—' %></td>
<td>
<% if (degree.status === 'active') { %>
<span class="badge bg-soft-success">Active</span>
<% } else { %>
<span class="badge bg-soft-danger">Revoked</span>
<% } %>
</td>
<td>
<div class="table-actions">
<a href="/admin/degree/<%= degree._id %>/edit" class="btn btn-sm btn-outline-primary btn-icon" title="Edit">
<i class="fas fa-pen"></i>
</a>
<form method="POST" action="/admin/degree/<%= degree._id %>/delete" style="display:inline;" onsubmit="return confirm('Delete this degree?')">
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-graduation-cap"></i></div>
<h5>No degrees found</h5>
<p>Try adjusting your filters or create a new degree.</p>
<a href="/admin/degree/create" class="btn btn-primary">
<i class="fas fa-plus"></i> Create First Degree
</a>
</div>
<% } %>
</div>
</div>

View File

@@ -1,126 +1,112 @@
<!-- Header section -->
<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);">Department Management</h1>
<p class="text-muted mb-0">Manage all departments in the system</p>
</div>
<div>
<a href="/admin/department/create" class="btn btn-primary">
<i class="fas fa-plus-circle me-1"></i>Create New Department
</a>
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Department Management</h1>
<p class="subtitle">All departments registered in the system</p>
</div>
</div>
<div class="row g-3">
<!-- Add Form -->
<div class="col-lg-4">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-plus-circle"></i> Add Department</h5>
</div>
<div class="card-body">
<form method="POST" action="/admin/department/create">
<div class="mb-3">
<label class="form-label">Department Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" placeholder="Enter department name..." required>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-plus"></i> Add Department
</button>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<% if (departments && departments.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<!-- Table -->
<div class="col-lg-8">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-building"></i> Departments</h5>
<span class="badge badge-soft-primary"><%= departments ? departments.length : 0 %> total</span>
</div>
<div class="card-body p-0">
<% if (departments && departments.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:50px">#</th>
<th>Department Name</th>
<th>Slug</th>
<th style="width:130px">Actions</th>
</tr>
</thead>
<tbody>
<% departments.forEach((department, index) => { %>
<tr>
<th scope="col" style="width: 50px">#</th>
<th scope="col">Department Name</th>
<th scope="col">Slug</th>
<th scope="col" style="width: 200px">Actions</th>
</tr>
</thead>
<tbody>
<% departments.forEach((department, index) => { %>
<tr>
<td><%= index + 1 %></td>
<td>
<span class="fw-medium"><%= department.name %></span>
</td>
<td>
<code class="text-muted"><%= department.slug %></code>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/department/edit/<%= department._id %>" class="btn btn-sm btn-outline-primary">
<i class="fas fa-edit me-1"></i>Edit
</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-custom-modal="open"
data-id="<%= department._id %>"
data-name="<%= department.name %>">
<i class="fas fa-trash-alt me-1"></i>Delete
<td style="color:var(--text-muted)"><%= index + 1 %></td>
<td>
<span class="dept-name-display-<%= department._id %>" style="font-weight:500"><%= department.name %></span>
<form method="POST" action="/admin/department/<%= department._id %>/edit"
class="dept-edit-form-<%= department._id %> d-none d-flex gap-2 align-items-center">
<input type="text" class="form-control form-control-sm" name="name" value="<%= department.name %>" required>
<button type="submit" class="btn btn-sm btn-outline-primary btn-icon" title="Save">
<i class="fas fa-check"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-icon" title="Cancel"
onclick="cancelEdit('<%= department._id %>')">
<i class="fas fa-times"></i>
</button>
</form>
</td>
<td><code style="font-size:0.8rem;color:var(--text-muted)"><%= department.slug %></code></td>
<td>
<div class="table-actions">
<button type="button" class="btn btn-sm btn-outline-primary btn-icon" title="Edit"
onclick="startEdit('<%= department._id %>')">
<i class="fas fa-pen"></i>
</button>
<form method="POST" action="/admin/department/<%= department._id %>/delete" style="display:inline;"
onsubmit="return confirm('Delete department \"<%= department.name %>\"?')">
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="text-center py-5">
<i class="fas fa-folder-open text-muted mb-3" style="font-size: 3rem;"></i>
<h5 class="text-muted mb-3">No Departments Found</h5>
<a href="/admin/department/create" class="btn btn-primary">
<i class="fas fa-plus-circle me-1"></i>Create First Department
</a>
</div>
<% } %>
</div>
</form>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-building"></i></div>
<h5>No departments yet</h5>
<p>Use the form on the left to add the first department.</p>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Custom Modal -->
<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 department?</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>
document.addEventListener('DOMContentLoaded', function() {
// Khởi tạo modal tùy chỉnh
CustomModal.init('customModal', {
closeOnOutsideClick: true,
animationDuration: 300
});
// Lắng nghe click vào nút xóa
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 id = button.getAttribute('data-id');
const name = button.getAttribute('data-name');
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
CustomModal.confirm(
`Are you sure you want to delete department "${name}"? This action cannot be undone.`,
function() {
// Hành động khi xác nhận
window.location.href = `/admin/department/delete/${id}`;
},
null,
'Delete Confirmation'
);
}
});
});
function startEdit(id) {
document.querySelector('.dept-name-display-' + id).classList.add('d-none');
const form = document.querySelector('.dept-edit-form-' + id);
form.classList.remove('d-none');
form.classList.add('d-flex');
}
function cancelEdit(id) {
document.querySelector('.dept-name-display-' + id).classList.remove('d-none');
const form = document.querySelector('.dept-edit-form-' + id);
form.classList.add('d-none');
form.classList.remove('d-flex');
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,661 +0,0 @@
<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);">Form Management</h1>
<p class="text-muted mb-0">Edit form content for Academics page</p>
</div>
<div>
<a href="/admin/academics" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-1"></i>Back to Academics
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form id="formManagementForm" action="/admin/form/update" method="POST" class="content-with-fixed-buttons">
<input type="hidden" name="admissionJson" id="admissionJson">
<input type="hidden" name="applyJson" id="applyJson">
<input type="hidden" name="applicationFormJson" id="applicationFormJson">
<!-- 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="#admission" role="tab">
<i class="fas fa-door-open me-2"></i>Admission
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#apply" role="tab">
<i class="fas fa-check-circle me-2"></i>Apply
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#application-form" role="tab">
<i class="fas fa-file-alt me-2"></i>Application Form
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Admission Tab -->
<div class="tab-pane fade show active" id="admission" role="tabpanel">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Background Image</label>
<div class="row">
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control" id="admissionBgImage" name="admissionBgImage" value="<%= form.admission && form.admission.background_image ? form.admission.background_image : '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="admissionBgImage" data-image-type="academics">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6">
<div class="image-preview mt-2">
<% if (form.admission && form.admission.background_image) { %>
<img src="<%= form.admission.background_image %>" alt="Background preview" class="img-thumbnail" style="height:200px;width:100%;object-fit:cover;">
<% } %>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="admissionTitle" name="admissionTitle" value="<%= form.admission && form.admission.title ? form.admission.title : '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Year</label>
<input type="text" class="form-control" id="admissionYear" name="admissionYear" value="<%= form.admission && form.admission.year ? form.admission.year : '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="admissionDescription" name="admissionDescription" rows="3"><%= form.admission && form.admission.description ? form.admission.description : '' %></textarea>
</div>
<!-- Admission Form Fields -->
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Form Fields</h5>
</div>
<div class="card-body">
<div id="admissionFormFields">
<% (form.admission && form.admission.form && form.admission.form.fields || []).forEach((field, index) => { %>
<div class="mb-3 border p-3">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Field Type</label>
<input type="text" class="form-control" value="<%= field.type %>">
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control" value="<%= field.placeholder %>">
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Form Button Text</label>
<input type="text" class="form-control" id="admissionFormButton" name="admissionFormButton" value="<%= form.admission && form.admission.form && form.admission.form.button ? form.admission.form.button.text : '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Form Button URL</label>
<input type="text" class="form-control" id="admissionFormButtonUrl" name="admissionFormButtonUrl" value="<%= form.admission && form.admission.form && form.admission.form.button ? form.admission.form.button.url : '' %>">
</div>
</div>
</div>
<!-- Apply Tab -->
<div class="tab-pane fade" id="apply" 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="applyTitle" name="applyTitle" value="<%= form.apply && form.apply.title ? form.apply.title : '' %>">
</div>
<!-- Apply Steps -->
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Application Steps</h5>
</div>
<div class="card-body">
<div id="applySteps">
<% (form.apply && form.apply.steps || []).forEach((step, index) => { %>
<div class="mb-3 border p-3">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" class="form-control" name="applyStepTitle_<%= index %>" value="<%= step.title %>">
</div>
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control" name="applyStepDescription_<%= index %>" rows="2"><%= step.description %></textarea>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Application Form Tab -->
<div class="tab-pane fade" id="application-form" 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="applicationFormTitle" name="applicationFormTitle" value="<%= form.application_form && form.application_form.title ? form.application_form.title : '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Question</label>
<input type="text" class="form-control" id="applicationFormQuestion" name="applicationFormQuestion" value="<%= form.application_form && form.application_form.question ? form.application_form.question : '' %>">
</div>
<!-- Button Section -->
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Button</h5>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label">Text</label>
<input type="text" class="form-control" id="applicationFormButtonText" name="applicationFormButtonText" value="<%= form.application_form && form.application_form.button ? form.application_form.button.text : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Icon</label>
<input type="text" class="form-control" id="applicationFormButtonIcon" name="applicationFormButtonIcon" value="<%= form.application_form && form.application_form.button ? form.application_form.button.icon : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">URL</label>
<input type="text" class="form-control" id="applicationFormButtonUrl" name="applicationFormButtonUrl" value="<%= form.application_form && form.application_form.button ? form.application_form.button.url : '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Links Section -->
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Links</h5>
</div>
<div class="card-body">
<div id="applicationFormLinks">
<% (form.application_form && form.application_form.links || []).forEach((link, index) => { %>
<div class="mb-3 border p-3">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label">Text</label>
<input type="text" class="form-control" name="applicationFormLinkText_<%= index %>" value="<%= link.text %>">
</div>
<div class="col-md-6">
<label class="form-label">URL</label>
<input type="text" class="form-control" name="applicationFormLinkUrl_<%= index %>" value="<%= link.url %>">
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</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"></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>
<!-- Thêm input file ẩn cho upload ảnh -->
<input type="file" id="directImageUpload" style="display: none;">
<input type="hidden" id="currentImageType" name="imageType">
<input type="hidden" id="currentTargetInput" name="targetInput">
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function() {
// Lưu trữ dữ liệu gốc để so sánh thay đổi
originalFormData = <%- JSON.stringify(form) %>;
// Khởi tạo form handlers
initializeFormHandlers();
});
function initializeFormHandlers() {
// Form submission
const form = document.querySelector('form');
form.addEventListener('submit', handleFormSubmit);
// Upload ảnh
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function() {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType || 'academics';
openImageUploader(targetInput, imageType);
});
});
// Khởi tạo direct image upload
initializeDirectImageUpload();
}
function initializeDirectImageUpload() {
const fileInput = document.getElementById('directImageUpload');
fileInput.addEventListener('change', handleDirectImageUpload);
}
function openImageUploader(targetInput, imageType) {
// Lưu thông tin upload hiện tại
document.getElementById('currentImageType').value = imageType;
document.getElementById('currentTargetInput').value = targetInput;
// Kích hoạt input file
const fileInput = document.getElementById('directImageUpload');
fileInput.click();
}
async function handleDirectImageUpload(e) {
if (!this.files || !this.files[0]) return;
const file = this.files[0];
const imageType = document.getElementById('currentImageType').value;
const targetInput = document.getElementById('currentTargetInput').value;
try {
// Disable nút upload và hiển thị loading
const uploadButton = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadButton) {
uploadButton.disabled = true;
uploadButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
// Tạo form data
const formData = new FormData();
formData.append('image', file);
// Upload ảnh
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.path) {
// Tìm input cần cập nhật, ưu tiên theo name, nếu không có thì tìm theo id
const inputElement = document.querySelector(`[name="${targetInput}"]`) || document.getElementById(targetInput);
if (inputElement) {
inputElement.value = result.path;
// Cập nhật preview
updateImagePreview(inputElement, result.path);
}
showToast('Success', 'Image uploaded successfully', 'success');
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
} finally {
// Reset nút upload
const uploadButton = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadButton) {
uploadButton.disabled = false;
uploadButton.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
}
// Reset file input
this.value = '';
}
}
function updateImagePreview(inputElement, imagePath) {
// Tìm hoặc tạo phần tử preview theo cách giống như Partnerships
let imgPreview = inputElement.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
const targetInput = inputElement.name || inputElement.id;
if (targetInput.toLowerCase().includes('logo')) {
img.style.height = '100px';
img.style.objectFit = 'contain';
} else {
img.style.height = '200px';
img.style.width = '100%';
img.style.objectFit = 'cover';
}
img.alt = 'Image preview';
imgPreview.appendChild(img);
inputElement.parentElement.parentElement.appendChild(imgPreview);
}
// Cập nhật đường dẫn hình ảnh
const img = imgPreview.querySelector('img');
if (img) {
img.src = imagePath;
}
}
function handleFormSubmit(e) {
e.preventDefault();
try {
console.log('=== Form Submit Debug ===');
// Chuẩn bị dữ liệu form
const formData = prepareFormData();
console.log('Complete form data:', formData);
// Log từng section
Object.keys(formData).forEach(key => {
console.log(`${key} data:`, formData[key]);
});
// Validate dữ liệu
const errors = validateFormData(formData);
if (errors.length > 0) {
console.log('Validation errors:', errors);
showToast('Error', errors.join('. '), 'error');
return;
}
// Log hidden inputs trước khi cập nhật
Object.keys(formData).forEach(key => {
const input = document.getElementById(`${key}Json`);
console.log(`Hidden input ${key}Json:`, {
element: input,
value: input?.value
});
});
// Cập nhật hidden inputs (map key -> input id)
const keyToInputId = {
admission: 'admissionJson',
apply: 'applyJson',
application_form: 'applicationFormJson'
};
Object.keys(formData).forEach(key => {
const inputId = keyToInputId[key] || `${key}Json`;
const input = document.getElementById(inputId);
if (input) {
const jsonValue = JSON.stringify(formData[key]);
input.value = jsonValue;
console.log(`Updated ${inputId} with:`, jsonValue);
} else {
console.warn(`Hidden input not found: ${inputId}`);
}
});
// Thông báo đang lưu
showToast('Info', 'Saving changes...', 'info');
// Log form final state
console.log('Form final state:', {
admissionJson: document.getElementById('admissionJson')?.value,
applyJson: document.getElementById('applyJson')?.value,
applicationFormJson: document.getElementById('applicationFormJson')?.value
});
// Submit form
this.submit();
} catch (error) {
console.error('Form submission error:', error);
console.error('Error stack:', error.stack);
showToast('Error', 'Error processing form: ' + error.message, 'error');
}
}
function prepareFormData() {
return {
admission: prepareAdmissionData(),
apply: prepareApplyData(),
application_form: prepareApplicationFormData()
};
}
function prepareAdmissionData() {
try {
// Basic info
const data = {
background_image: document.getElementById('admissionBgImage')?.value || '',
title: document.getElementById('admissionTitle')?.value || '',
year: document.getElementById('admissionYear')?.value || '',
description: document.getElementById('admissionDescription')?.value || '',
form: {
fields: [],
button: {
text: document.getElementById('admissionFormButton')?.value || '',
url: document.getElementById('admissionFormButtonUrl')?.value || '#'
}
}
};
// Form fields
const formFields = document.querySelectorAll('#admissionFormFields > div');
if (formFields) {
data.form.fields = Array.from(formFields).map(field => {
const inputs = field.querySelectorAll('input');
return {
type: inputs[0]?.value || '',
placeholder: inputs[1]?.value || ''
};
}).filter(field => field.type || field.placeholder);
}
return data;
} catch (error) {
console.error('Error preparing admission data:', error);
return {
background_image: '',
title: '',
year: '',
description: '',
form: {
fields: [],
button: {
text: '',
url: '#'
}
}
};
}
}
function prepareApplyData() {
try {
const data = {
title: document.getElementById('applyTitle')?.value || '',
steps: []
};
// Steps
const steps = document.querySelectorAll('#applySteps > div');
if (steps) {
data.steps = Array.from(steps).map(step => ({
title: step.querySelector('input[name^="applyStepTitle_"]')?.value || '',
description: step.querySelector('textarea[name^="applyStepDescription_"]')?.value || ''
})).filter(step => step.title || step.description);
}
return data;
} catch (error) {
console.error('Error preparing apply data:', error);
return {
title: '',
steps: []
};
}
}
function prepareApplicationFormData() {
try {
console.log('=== Debug Application Form Data ===');
// Log các elements
console.log('Title element:', document.getElementById('applicationFormTitle'));
console.log('Question element:', document.getElementById('applicationFormQuestion'));
console.log('Button text element:', document.getElementById('applicationFormButtonText'));
console.log('Button icon element:', document.getElementById('applicationFormButtonIcon'));
const data = {
title: document.getElementById('applicationFormTitle')?.value || '',
question: document.getElementById('applicationFormQuestion')?.value || '',
button: {
text: document.getElementById('applicationFormButtonText')?.value || '',
icon: document.getElementById('applicationFormButtonIcon')?.value || '',
url: document.getElementById('applicationFormButtonUrl')?.value || '#'
},
links: []
};
// Log giá trị cơ bản
console.log('Basic data:', {
title: data.title,
question: data.question,
button: data.button
});
// Debug links
const linksContainer = document.getElementById('applicationFormLinks');
console.log('Links container:', linksContainer);
const linkDivs = document.querySelectorAll('#applicationFormLinks > div');
console.log('Found link divs:', linkDivs.length);
if (linkDivs.length > 0) {
data.links = Array.from(linkDivs).map((link, index) => {
const textInput = link.querySelector('input[name^="applicationFormLinkText_"]');
const urlInput = link.querySelector('input[name^="applicationFormLinkUrl_"]');
return {
text: textInput?.value || '',
url: urlInput?.value || ''
};
}).filter(link => link.text || link.url);
}
console.log('Final application form data:', data);
return data;
} catch (error) {
console.error('Error preparing application form data:', error);
console.error('Error stack:', error.stack);
return {
title: '',
question: '',
button: {
text: '',
icon: '',
url: '#'
},
links: []
};
}
}
function validateFormData(data) {
const errors = [];
// Validate admission
if (!data.admission.title) errors.push('Admission title is required');
if (!data.admission.year) errors.push('Admission year is required');
// Validate apply
if (!data.apply.title) errors.push('Apply title is required');
if (!Array.isArray(data.apply.steps)) errors.push('Apply steps must be an array');
// Validate application form
if (!data.application_form.title) errors.push('Application form title is required');
if (!data.application_form.question) errors.push('Application form question is required');
return errors;
}
function showToast(title, message, type = 'info') {
if (window.toastManager) {
window.toastManager[type](message);
return;
}
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}:</strong> ${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
// Add toast to container
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
}
container.appendChild(toast);
// Show toast
const bsToast = new bootstrap.Toast(toast, {
animation: true,
autohide: true,
delay: 3000
});
bsToast.show();
// Remove toast after hidden
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,355 +0,0 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-sitemap me-2"></i>Menu Structure
</h6>
<button class="btn btn-primary btn-sm" onclick="prepareAddMenu()">
<i class="fas fa-plus me-1"></i>Add Root Menu
</button>
</div>
<div class="card-body p-0">
<div id="nestedMenuContainer" class="nested-menu-container">
<ul id="menuRoot" class="list-unstyled menu-group" data-id="root">
<% function renderMenu(items) { %>
<% items.forEach(item => { %>
<li class="menu-item-wrapper mb-2" data-id="<%= item._id %>">
<div class="menu-item-row d-flex align-items-center p-3 rounded border hover-shadow-sm bg-white">
<div class="menu-drag-handle me-2 text-muted">
<i class="fas fa-grip-vertical"></i>
</div>
<div class="menu-toggle-icon me-2">
<% if (item.children && item.children.length > 0) { %>
<button class="btn btn-sm p-0 btn-toggle-nested" type="button">
<i class="fas fa-chevron-down transition-base text-muted" style="width: 12px;"></i>
</button>
<% } else { %>
<div style="width: 24px;"></div>
<% } %>
</div>
<div class="menu-icon me-3">
<i class="fas <%= (item.type === 'external' ? 'fa-external-link-alt text-info' : 'fa-link text-secondary') %>"></i>
</div>
<div class="flex-grow-1">
<div class="d-flex align-items-center">
<span class="fw-bold text-dark"><%= item.title %></span>
<% if (item.type === 'external') { %>
<span class="badge bg-light text-dark border ms-2" style="font-size: 0.7rem;">External</span>
<% } %>
<% if (item.status === 'inactive') { %>
<span class="badge ms-2 bg-soft-danger text-danger">Inactive</span>
<% } else { %>
<span class="badge bg-soft-success ms-2">Active</span>
<% } %>
</div>
<div class="text-muted small text-truncate" style="max-width: 300px;">
<i class="fas fa-link me-1" style="font-size: 0.75rem;"></i><%= item.url %>
</div>
</div>
<div class="menu-actions">
<div class="btn-group-action">
<button class="btn btn-sm btn-add-child" data-id="<%= item._id %>" title="Add Sub-menu">
<i class="fas fa-plus text-action-add"></i>
</button>
<button class="btn btn-sm btn-edit-menu" data-item='<%= JSON.stringify(item).replace(/'/g, "&apos;") %>' title="Edit">
<i class="fas fa-edit text-action-edit"></i>
</button>
<button type="button" class="btn btn-sm btn-delete-menu" data-id="<%= item._id %>" data-title="<%= item.title %>" title="Delete">
<i class="fas fa-trash text-action-delete"></i>
</button>
</div>
<form id="delete-form-<%= item._id %>" action="/admin/header/menu/delete" method="POST" class="d-none">
<input type="hidden" name="id" value="<%= item._id %>">
</form>
</div>
</div>
<ul class="list-unstyled menu-group mt-2 ps-4 ms-2 border-start nested-list" style="min-height: 5px;" data-id="<%= item._id %>">
<% if (item.children && item.children.length > 0) { %>
<% renderMenu(item.children) %>
<% } %>
</ul>
</li>
<% }) %>
<% } %>
<% if (menuData.tree && menuData.tree.length > 0) { %>
<% renderMenu(menuData.tree) %>
<% } %>
</ul>
<% if (!menuData.tree || menuData.tree.length === 0) { %>
<div class="text-center py-5">
<img src="/assets/img/icon/empty-state.svg" alt="Empty" style="width: 120px; opacity: 0.5;" class="mb-3">
<p class="text-muted">No menu items found. Start by adding a root menu.</p>
</div>
<% } %>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
console.log('Menu Tab JS Initialized');
initSortable();
function initSortable() {
const menuGroups = document.querySelectorAll('.menu-group');
menuGroups.forEach(group => {
new Sortable(group, {
group: 'nested-menu',
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
handle: '.menu-drag-handle',
ghostClass: 'menu-ghost',
chosenClass: 'menu-chosen',
dragClass: 'menu-dragging',
onEnd: function (evt) {
console.log('Drag ended', evt);
// Highlight that changes need saving
const saveBtn = document.getElementById('saveHeaderBtn');
if (saveBtn && typeof window.markHeaderChanged === 'function') {
window.markHeaderChanged();
}
}
});
});
}
// Bind Edit/Add/Delete buttons
bindMenuActions();
function bindMenuActions() {
// Use event delegation for better performance and to handle re-rendered items if any
const container = document.getElementById('nestedMenuContainer');
if (container) {
container.addEventListener('click', function(e) {
const editBtn = e.target.closest('.btn-edit-menu');
if (editBtn) {
try {
const item = JSON.parse(editBtn.dataset.item);
console.log('=== TRACE: Edit Menu Clicked ===', item);
prepareEditMenu(item);
} catch (e) {
console.error('Error parsing menu item data:', e);
}
return;
}
const addChildBtn = e.target.closest('.btn-add-child');
if (addChildBtn) {
const pid = addChildBtn.dataset.id;
console.log('=== TRACE: Add Child Clicked ===', { pid });
if (typeof prepareAddChild === 'function') {
prepareAddChild(pid);
} else {
console.error('prepareAddChild function not found');
}
return;
}
const deleteBtn = e.target.closest('.btn-delete-menu');
if (deleteBtn) {
const id = deleteBtn.dataset.id;
const title = deleteBtn.dataset.title;
console.log('=== TRACE: Delete Menu Clicked ===', { id, title });
if (confirm(`Are you sure you want to delete "${title}" and all its sub-menu items?`)) {
const form = document.getElementById('delete-form-' + id);
console.log('=== TRACE: Submitting Delete Form ===', form ? form.action : 'FORM NOT FOUND');
if (form) form.submit();
}
return;
}
const toggleBtn = e.target.closest('.btn-toggle-nested');
if (toggleBtn) {
const wrapper = toggleBtn.closest('.menu-item-wrapper');
const nestedUl = wrapper.querySelector('.nested-list');
const icon = toggleBtn.querySelector('i');
if (nestedUl) {
const isCollapsed = nestedUl.classList.toggle('collapsed');
if (isCollapsed) {
icon.style.transform = 'rotate(-90deg)';
} else {
icon.style.transform = 'rotate(0deg)';
}
}
}
});
}
}
});
function prepareAddMenu() {
console.log('=== TRACE: prepareAddMenu Called ===');
const form = document.getElementById('menuForm');
if (!form) return;
form.action = '/admin/header/menu/create';
document.getElementById('modalTitle').innerText = 'Add Root Menu';
document.getElementById('menuId').value = '';
document.getElementById('parentId').value = '';
document.getElementById('formTitle').value = '';
document.getElementById('formUrl').value = '';
document.getElementById('formOrder').value = '0';
document.getElementById('formStatus').value = 'active';
document.getElementById('typeInternal').checked = true;
const modalElement = document.getElementById('modalAddMenu');
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
modal.show();
}
function prepareAddChild(parentId) {
prepareAddMenu();
document.getElementById('parentId').value = parentId;
document.getElementById('modalTitle').innerText = 'Add Sub-Menu Item';
}
function prepareEditMenu(item) {
const form = document.getElementById('menuForm');
if (!form) return;
form.action = '/admin/header/menu/update/' + item._id;
document.getElementById('modalTitle').innerText = 'Edit Menu Item';
document.getElementById('menuId').value = item._id;
document.getElementById('parentId').value = item.parentId || '';
document.getElementById('formTitle').value = item.title;
document.getElementById('formUrl').value = item.url;
document.getElementById('formOrder').value = item.order;
document.getElementById('formStatus').value = item.status;
if (item.type === 'external') {
document.getElementById('typeExternal').checked = true;
} else {
document.getElementById('typeInternal').checked = true;
}
const modalElement = document.getElementById('modalAddMenu');
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
modal.show();
}
function collectMenuData() {
const items = [];
function traverse(element, parentId = null) {
const children = element.children;
for (let i = 0; i < children.length; i++) {
const li = children[i];
if (li.tagName !== 'LI') continue;
const id = li.dataset.id;
if (!id) continue;
items.push({
id: id,
order: i + 1,
parentId: parentId === 'root' ? null : parentId
});
const subUl = li.querySelector('.menu-group');
if (subUl) {
traverse(subUl, id);
}
}
}
const rootUl = document.getElementById('menuRoot');
if (rootUl) traverse(rootUl, 'root');
return items;
}
window.saveMenuChanges = function(showToastFlag = true) {
return new Promise((resolve, reject) => {
console.log('=== TRACE: saveMenuChanges Called (Reorder) ===');
const items = collectMenuData();
if (items.length === 0) {
console.warn('No menu items found to reorder');
return resolve({ success: true, message: 'No items' });
}
const saveBtn = document.getElementById('saveHeaderBtn');
const originalHtml = saveBtn ? saveBtn.innerHTML : '';
// Only manage button state if this is a direct call (not unified save)
const manageButton = showToastFlag && saveBtn;
if (manageButton) {
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving Menu...';
}
fetch('/admin/header/menu/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: items })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (showToastFlag) showNotification('Menu structure saved', 'success');
resolve(data);
} else {
reject(new Error(data.message));
}
})
.catch(error => reject(error))
.finally(() => {
if (manageButton) {
saveBtn.disabled = false;
saveBtn.innerHTML = originalHtml;
}
});
});
};
window.collectMenuData = collectMenuData;
// Global reset fallback
window.prepareAddMenu = prepareAddMenu;
</script>
<!-- Redundant buttons removed to use global buttons in index.ejs -->
<style>
.nested-menu-container {
padding: var(--spacing-3);
}
.menu-group {
min-height: 10px;
}
.menu-item-row {
transition: var(--transition-base);
}
.menu-drag-handle { cursor: grab; padding: 5px; }
.menu-drag-handle:active { cursor: grabbing; }
/* SortableJS Classes mapped to our variables */
.menu-ghost {
opacity: 0.4;
border: 2px dashed var(--primary-color) !important;
}
.menu-chosen {
background-color: var(--primary-soft) !important;
}
.menu-dragging {
opacity: 0.9;
}
/* Collapse/Expand Styles */
.nested-list {
overflow: hidden;
transition: all 0.3s ease;
max-height: 2000px; /* Large enough for nested items */
}
.nested-list.collapsed {
max-height: 0;
margin-top: 0 !important;
opacity: 0;
pointer-events: none;
}
.btn-toggle-nested i {
transition: transform 0.2s ease;
}
.transition-base {
transition: all 0.2s ease-in-out;
}
</style>

View File

@@ -1,216 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Manage FAQ section content</p>
</div>
<div>
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Homepage
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="faqForm" action="/admin/faq/update">
<!-- Hidden input for items JSON data -->
<input type="hidden" name="items" id="itemsJson">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-question-circle me-2"></i>FAQ Settings</h5>
</div>
<div class="card-body">
<!-- Header Section -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" name="heading" id="heading"
value="<%= data.heading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" name="subheading" id="subheading"
value="<%= data.subheading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" name="description" id="description"
rows="2"><%= data.description || '' %></textarea>
</div>
</div>
<!-- CTA Button Section -->
<h6 class="fw-medium mb-3">CTA Button</h6>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Button Label</label>
<input type="text" class="form-control" name="ctaLabel"
value="<%= data.ctaButton && data.ctaButton.label ? data.ctaButton.label : '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Button Link</label>
<input type="text" class="form-control" name="ctaHref"
value="<%= data.ctaButton && data.ctaButton.href ? data.ctaButton.href : '' %>">
</div>
</div>
<hr class="my-4">
<!-- FAQ Items -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Frequently Asked Questions</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFaqItem()">
<i class="fas fa-plus"></i> Add Question
</button>
</div>
<div id="faqItemsContainer">
<% if (data.items && data.items.length> 0) { %>
<% data.items.forEach((item, index)=> { %>
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control"
name="itemQuestion_<%= index %>"
value="<%= item.question || '' %>">
</div>
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control" name="itemAnswer_<%= index %>"
rows="3"><%= item.answer || '' %></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFaqItem(this)">
<i class="fas fa-trash me-2"></i>Remove
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="faqDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('faqDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = { items: [] };
}
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('faqForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateItemsJson();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
}
function updateItemsJson() {
const items = [];
document.querySelectorAll('.faq-item').forEach((item, index) => {
const question = item.querySelector(`[name="itemQuestion_${index}"]`)?.value ||
item.querySelector('[name^="itemQuestion_"]')?.value || '';
const answer = item.querySelector(`[name="itemAnswer_${index}"]`)?.value ||
item.querySelector('[name^="itemAnswer_"]')?.value || '';
items.push({ question, answer });
});
document.getElementById('itemsJson').value = JSON.stringify(items);
}
function addFaqItem() {
const container = document.getElementById('faqItemsContainer');
const index = container.querySelectorAll('.faq-item').length;
const html = `
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control" name="itemQuestion_${index}" value="">
</div>
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control" name="itemAnswer_${index}" rows="3"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFaqItem(this)">
<i class="fas fa-trash me-2"></i>Remove
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFaqItem(button) {
if (confirm('Are you sure you want to remove this question?')) {
button.closest('.faq-item').remove();
reindexItems();
}
}
function reindexItems() {
document.querySelectorAll('.faq-item').forEach((item, index) => {
item.querySelectorAll('[name^="item"]').forEach(input => {
const baseName = input.name.replace(/_\d+$/, '');
input.name = `${baseName}_${index}`;
});
});
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
</script>

View File

@@ -1,231 +0,0 @@
<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)">
Homepage Management
</h1>
<p class="text-muted mb-0">Edit content displayed on homepage</p>
</div>
<div>
<a href="<%= frontendUrl %>" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Homepage
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/home/update" method="POST" class="content-with-fixed-buttons">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson" />
<input type="hidden" name="whyChooseUs" id="whyChooseUsJson" />
<input type="hidden" name="visaSolutions" id="visaSolutionsJson" />
<input type="hidden" name="visaCountries" id="visaCountriesJson" />
<input type="hidden" name="testimonials" id="testimonialsJson" />
<input type="hidden" name="videoGallery" id="videoGalleryJson" />
<input type="hidden" name="faq" id="faqJson" />
<input type="hidden" name="achievements" id="achievementsJson" />
<input type="hidden" name="partners" id="partnersJson" />
<input type="hidden" name="blogPreview" id="blogPreviewJson" />
<!-- 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="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#whychooseus" role="tab">
<i class="fas fa-star me-2"></i>Why Choose Us
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#visasolutions" role="tab">
<i class="fas fa-concierge-bell me-2"></i>Visa Solutions
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#visacountries" role="tab">
<i class="fas fa-globe-americas me-2"></i>Visa Countries
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
<i class="fas fa-comments me-2"></i>Testimonials
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#videogallery" role="tab">
<i class="fas fa-video me-2"></i>Video Gallery
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#faq" role="tab">
<i class="fas fa-question-circle me-2"></i>FAQ
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#achievements" role="tab">
<i class="fas fa-chart-pie me-2"></i>Achievements
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#partners" role="tab">
<i class="fas fa-handshake me-2"></i>Partners
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#blogpreview" role="tab">
<i class="fas fa-blog me-2"></i>Blog Preview
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<%- include('sections/hero') %>
<%- include('sections/whyChooseUs') %>
<%- include('sections/visaSolutions') %>
<%- include('sections/visaCountries') %>
<%- include('sections/testimonials') %>
<%- include('sections/videoGallery') %>
<%- include('sections/faq') %>
<%- include('sections/achievements') %>
<%- include('sections/partners') %>
<%- include('sections/blogPreview') %>
</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>
<!-- Image upload input -->
<input type="file" id="directImageUpload" style="display: none" />
<input type="hidden" id="currentImageType" name="imageType" />
<input type="hidden" id="currentTargetInput" name="targetInput" />
<script>
/**
* BRIDGE SCRIPT: Cho phép các section lẻ tự đăng ký logic lấy dữ liệu.
* Cách dùng trong file lẻ (vị dụ hero.ejs):
* <script>
* window.homeScrapers = window.homeScrapers || {};
* window.homeScrapers.hero = () => ({ title: document.getElementById('heroTitle').value, ... });
* <\/script>
*/
window.homeScrapers = window.homeScrapers || {};
document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector("form");
if (form) {
form.addEventListener("submit", function (e) {
console.log("Form submitting, collecting data from scrapers...");
// Tự động thu gom dữ liệu từ các section đã đăng ký
Object.keys(window.homeScrapers).forEach(section => {
const input = document.getElementById(section + 'Json');
if (input) {
try {
const data = window.homeScrapers[section]();
console.log(`- Collected data for [${section}]:`, data);
input.value = JSON.stringify(data);
} catch (err) {
console.error(`Error scraping section [${section}]:`, err);
}
}
});
// Để form tự submit tự nhiên sau khi đã điền xong các hidden inputs
});
}
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
initImageUploads();
});
// --- UTILITIES (Dùng chung) ---
function initImageUploads() {
document.addEventListener("click", function (e) {
const btn = e.target.closest(".btn-upload-image");
if (btn) {
document.getElementById("currentImageType").value = btn.dataset.imageType;
document.getElementById("currentTargetInput").value = btn.dataset.targetInput;
document.getElementById("directImageUpload").click();
}
});
const fileInput = document.getElementById("directImageUpload");
if (fileInput) {
fileInput.addEventListener("change", handleDirectImageUpload);
}
}
async function handleDirectImageUpload(e) {
if (!this.files || !this.files[0]) return;
const file = this.files[0];
const imageType = document.getElementById("currentImageType").value;
const targetInput = document.getElementById("currentTargetInput").value;
try {
const formData = new FormData();
formData.append("image", file);
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: "POST", body: formData });
const result = await response.json();
if (result.success && result.path) {
const input = document.getElementById(targetInput);
if (input) {
input.value = result.path;
// Cập nhật preview nếu có img ngay sau input group
const previewImg = input.closest('.input-group')?.nextElementSibling?.querySelector('img');
if (previewImg) {
previewImg.src = result.path;
previewImg.classList.remove('d-none');
}
}
showToast("Success", "Image uploaded successfully", "success");
} else {
throw new Error(result.error || "Upload failed");
}
} catch (error) {
showToast("Error", "Upload failed: " + error.message, "error");
}
this.value = "";
}
function showToast(title, message, type = "info") {
let container = document.querySelector(".toast-container") || (() => {
const c = document.createElement("div");
c.className = "toast-container position-fixed top-0 end-0 p-3";
document.body.appendChild(c);
return c;
})();
const toast = document.createElement("div");
toast.className = `toast align-items-center text-white bg-${type === "error" ? "danger" : type} border-0`;
toast.setAttribute("role", "alert");
toast.innerHTML = `<div class="d-flex"><div class="toast-body"><strong>${title}:</strong> ${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
container.appendChild(toast);
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
toast.addEventListener("hidden.bs.toast", () => toast.remove());
}
</script>

View File

@@ -1,122 +0,0 @@
<!-- Achievements Tab -->
<div class="tab-pane fade" id="achievements" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2"></i>General Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input
type="text"
class="form-control"
id="achievementsHeading"
value="<%= data.achievements?.heading || '' %>"
placeholder="e.g., Our Achievements in Numbers"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input
type="text"
class="form-control"
id="achievementsSubheading"
value="<%= data.achievements?.subheading || '' %>"
placeholder="e.g., Did You Know"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Achievement Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-list-ul me-2"></i>Achievement Items (Fixed 4 Items)
</h6>
</div>
<div class="card-body" id="achievementItemsContainer">
<% for(let i=0; i<4; i++) {
const item = (data.achievements?.items && data.achievements.items[i]) || {};
%>
<div class="card mb-3 bg-light border achievement-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="card-title fw-bold mb-0">Achievement #<%= i + 1 %></h6>
</div>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label fw-medium">Value (Number)</label>
<input
type="text"
class="form-control achievement-value"
value="<%= item.value || '' %>"
placeholder="e.g., 95"
/>
</div>
<div class="col-md-3">
<label class="form-label fw-medium">Suffix</label>
<input
type="text"
class="form-control achievement-suffix"
value="<%= item.suffix || '' %>"
placeholder="e.g., %"
/>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input
type="text"
class="form-control achievement-label"
value="<%= item.label || '' %>"
placeholder="e.g., Visa Success Rate"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea
class="form-control achievement-description"
rows="2"
placeholder="Short description of this achievement"
><%= item.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<script>
// Đăng ký scraper cho phần achievements
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.achievements = function() {
const items = [];
document.querySelectorAll('.achievement-item').forEach(el => {
items.push({
value: el.querySelector('.achievement-value').value,
suffix: el.querySelector('.achievement-suffix').value,
label: el.querySelector('.achievement-label').value,
description: el.querySelector('.achievement-description').value
});
});
return {
heading: document.getElementById('achievementsHeading').value,
subheading: document.getElementById('achievementsSubheading').value,
items: items
};
};
</script>

View File

@@ -1,175 +0,0 @@
<!-- Blog Preview Tab -->
<div class="tab-pane fade" id="blogpreview" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information & Blog Selection
</h6>
<span class="badge bg-info text-dark">CMS will automatically fetch the 3 latest posts if no specific blog is
selected.</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="blogPreviewHeading"
value="<%= data.blogPreview?.heading || '' %>" placeholder="e.g., Latest Insights & Updates" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="blogPreviewSubheading"
value="<%= data.blogPreview?.subheading || '' %>" placeholder="e.g., Visa Tips & Guides" />
</div>
<div class="col-md-12 mt-4">
<label class="form-label fw-bold"><i class="fas fa-check-square me-2"></i>Select Featured Blogs (Direct
from Blog Module)</label>
<p class="text-muted small mb-3">Select blog posts to prioritize on the home page. If none are selected,
the system will use the 3 latest posts.</p>
<div class="row g-3 blog-selector-container"
style="max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 15px; border-radius: 8px;">
<% if (allBlogs && allBlogs.length> 0) { %>
<% allBlogs.forEach(blog=> {
const isSelected = data.blogPreview?.selectedBlogIds && data.blogPreview.selectedBlogIds.some(id =>
id.toString() === blog._id.toString());
%>
<div class="col-md-4">
<div class="card h-100 blog-select-card <%= isSelected ? 'border-primary bg-light' : '' %>"
onclick="toggleBlogSelection(this, '<%= blog._id %>')"
style="cursor: pointer; transition: all 0.2s;">
<div class="position-absolute top-0 end-0 m-2">
<div class="form-check">
<input class="form-check-input blog-checkbox" type="checkbox" value="<%= blog._id %>"
<%=isSelected ? 'checked' : '' %> onclick="event.stopPropagation();
handleCheckboxChange(this)">
</div>
</div>
<img
src="<%= blog.featuredImage ? getFullImageUrl(blog.featuredImage, backendUrl) : '/assets/img/placeholder.jpg' %>"
class="card-img-top" style="height: 210px; object-fit: cover;">
<div class="card-body p-2">
<h6 class="card-title small fw-bold mb-1" title="<%= blog.title %>"
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; height: 2.6em; line-height: 1.3em;">
<%= blog.title %>
</h6>
<p class="card-text tiny text-muted mb-0">
<%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %>
</p>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="col-12 text-center py-4">
<p class="text-muted">No published blogs found. Please create some blogs first.</p>
<a href="/admin/blog/create" class="btn btn-sm btn-outline-primary">Create Blog</a>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="blogPreviewCtaLabel"
value="<%= data.blogPreview?.ctaButton?.label || '' %>" placeholder="e.g., View All Articles" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="blogPreviewCtaHref"
value="<%= data.blogPreview?.ctaButton?.href || '' %>" placeholder="/blog" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function toggleBlogSelection(card, blogId) {
const checkbox = card.querySelector('.blog-checkbox');
const isChecking = !checkbox.checked;
if (isChecking) {
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
if (checkedCount >= 3) {
alert('You can only select up to 3 blogs.');
return;
}
}
checkbox.checked = isChecking;
handleCheckboxUpdate(card, checkbox.checked);
}
function handleCheckboxChange(checkbox) {
if (checkbox.checked) {
const checkedCount = document.querySelectorAll('.blog-checkbox:checked').length;
if (checkedCount > 3) {
checkbox.checked = false;
alert('You can only select up to 3 blogs.');
return;
}
}
const card = checkbox.closest('.blog-select-card');
handleCheckboxUpdate(card, checkbox.checked);
}
function handleCheckboxUpdate(card, isChecked) {
if (isChecked) {
card.classList.add('border-primary', 'bg-light');
} else {
card.classList.remove('border-primary', 'bg-light');
}
}
// Đăng ký scraper cho Blog Preview
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.blogPreview = () => {
const selectedIds = [];
document.querySelectorAll('.blog-checkbox:checked').forEach(cb => {
selectedIds.push(cb.value);
});
// Chúng ta vẫn giữ cấu trúc cũ cho items nếu muốn preview offline,
// nhưng server sẽ ưu tiên dùng selectedBlogIds để populate dữ liệu mới nhất.
return {
heading: document.getElementById('blogPreviewHeading').value,
subheading: document.getElementById('blogPreviewSubheading').value,
ctaButton: {
label: document.getElementById('blogPreviewCtaLabel').value,
href: document.getElementById('blogPreviewCtaHref').value
},
selectedBlogIds: selectedIds,
items: [] // Server side will handle full items content
};
};
</script>
<style>
.tiny {
font-size: 0.75rem;
}
.blog-select-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -1,148 +0,0 @@
<!-- FAQ Tab -->
<div class="tab-pane fade" id="faq" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="faqHeading" value="<%= data.faq?.heading || '' %>"
placeholder="e.g., Got Questions? We've Got Answers" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="faqSubheading" value="<%= data.faq?.subheading || '' %>"
placeholder="e.g., Visa FAQs" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="faqDescription" rows="3"
placeholder="Enter description"><%= data.faq?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- FAQ Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-question-circle me-2"></i>FAQ Items
</h6>
</div>
<div class="card-body" id="faqItemsContainer">
<%
const faqItems = (data.faq && Array.isArray(data.faq.items) && data.faq.items.length === 5)
? data.faq.items
: (data.faq && Array.isArray(data.faq.items) && data.faq.items.length > 0)
? (function () {
const clone = data.faq.items.slice(0, 5);
while (clone.length < 5) clone.push({});
return clone;
})()
: [{}, {}, {}, {}, {}];
%>
<% faqItems.forEach(function(item, index) { %>
<div class="card mb-3 bg-light border faq-item" data-index="<%= index %>">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h6 class="card-title fw-bold mb-0">FAQ
<span class="faq-item-label">
<%= index + 1 %>
</span>
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Question</label>
<input
type="text"
class="form-control"
id="faqQuestion_<%= index %>"
value="<%= item.question || '' %>"
placeholder="Enter question"
/>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Answer</label>
<textarea
class="form-control"
id="faqAnswer_<%= index %>"
rows="3"
placeholder="Enter answer"
><%= item.answer || '' %></textarea>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="faqCtaLabel" value="<%= data.faq?.ctaButton?.label || '' %>"
placeholder="e.g., contact us" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="faqCtaHref" value="<%= data.faq?.ctaButton?.href || '' %>"
placeholder="/contact" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect FAQ data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.faq = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
const items = [];
document.querySelectorAll(".faq-item").forEach((el, idx) => {
const index = el.getAttribute("data-index") || idx;
const question = getVal(`faqQuestion_${index}`);
const answer = getVal(`faqAnswer_${index}`);
if (question || answer) {
items.push({ question, answer });
}
});
return {
heading: getVal("faqHeading"),
subheading: getVal("faqSubheading"),
description: getVal("faqDescription"),
ctaButton: {
label: getVal("faqCtaLabel"),
href: getVal("faqCtaHref")
},
items
};
};
</script>

View File

@@ -1,312 +0,0 @@
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="row g-4">
<!-- Background Image (section-level) -->
<div class="col-md-12">
<div class="card border shadow-sm mb-3">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-image me-2"></i>Hero Background
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/assets/img/home-1/hero/bg.jpg" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.hero?.backgroundImage) { %>
<div class="mt-2">
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Background preview" />
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Slides -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-sliders-h me-2"></i>Hero Slides
</h6>
<button type="button" class="btn btn-sm btn-primary" id="addHeroSlideBtn">
<i class="fas fa-plus me-1"></i>Add Slide
</button>
</div>
<div class="card-body" id="heroSlidesContainer">
<% const existingSlides=(data.hero && Array.isArray(data.hero.slides) && data.hero.slides.length> 0)
? data.hero.slides
: [{
title: data.hero?.title || '',
subtitle: data.hero?.subtitle || '',
description: data.hero?.description || '',
heroImage: data.hero?.heroImage || '',
videoUrl: data.hero?.videoUrl || '',
primaryButton: data.hero?.primaryButton || {},
secondaryButton: data.hero?.secondaryButton || {},
}];
%>
<% existingSlides.forEach(function(slide, index) { %>
<div class="card mb-3 border hero-slide-item" data-index="<%= index %>">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<span class="fw-semibold">Slide
<span class="hero-slide-label">
<%= index + 1 %>
</span>
</span>
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-hero-slide">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_title"
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3"
placeholder="Enter hero description"><%= slide.description || '' %></textarea>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Hero Image</label>
<small class="text-muted d-block mb-1">Recommended size: 893x848px</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
value="<%= slide.heroImage || '' %>" placeholder="/assets/img/home-1/hero/man.png" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (slide.heroImage) { %>
<div class="mt-2">
<img src="<%= slide.heroImage %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Hero image preview" />
</div>
<% } %>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video URL</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
value="<%= slide.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
</div>
<!-- Primary Button -->
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<h6 class="fw-semibold mb-3">
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
</h6>
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel"
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref"
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact" />
</div>
</div>
</div>
</div>
<!-- Secondary Button -->
<div class="col-md-6">
<div class="border rounded p-3 h-100">
<h6 class="fw-semibold mb-3">
<i class="fas fa-mouse-pointer me-2"></i>Secondary Button
</h6>
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel"
value="<%= slide.secondaryButton?.label || '' %>"
placeholder="e.g., Book Free Consultation" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref"
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect Hero data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.hero = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
const backgroundImage = getVal("heroBackgroundImage");
const slides = [];
const slideEls = document.querySelectorAll(".hero-slide-item");
slideEls.forEach((el, idx) => {
const index = el.getAttribute("data-index") || idx;
const prefix = `heroSlide_${index}_`;
const slide = {
title: getVal(prefix + "title"),
subtitle: getVal(prefix + "subtitle"),
description: getVal(prefix + "description"),
heroImage: getVal(prefix + "heroImage"),
videoUrl: getVal(prefix + "videoUrl"),
primaryButton: {
label: getVal(prefix + "primaryLabel"),
href: getVal(prefix + "primaryHref")
},
secondaryButton: {
label: getVal(prefix + "secondaryLabel"),
href: getVal(prefix + "secondaryHref")
}
};
// Bỏ qua slide trống hoàn toàn
const hasContent = slide.title || slide.subtitle || slide.description || slide.heroImage || slide.videoUrl || slide.primaryButton.label || slide.secondaryButton.label;
if (hasContent) {
slides.push(slide);
}
});
// Legacy single-slide fields: map từ slide đầu tiên (nếu có) để không vỡ FE cũ
const first = slides[0] || {};
return {
backgroundImage,
slides,
title: first.title || "",
subtitle: first.subtitle || "",
description: first.description || "",
heroImage: first.heroImage || "",
videoUrl: first.videoUrl || "",
primaryButton: first.primaryButton || {
label: "",
href: ""
},
secondaryButton: first.secondaryButton || {
label: "",
href: ""
}
};
};
// Add / remove slides on the client
document.addEventListener("DOMContentLoaded", function () {
const container = document.getElementById("heroSlidesContainer");
const addBtn = document.getElementById("addHeroSlideBtn");
if (!container || !addBtn)
return;
// Re-index slides, update labels, IDs and upload target attributes
const updateLabels = () => {
container.querySelectorAll(".hero-slide-item").forEach((el, idx) => {
el.setAttribute("data-index", String(idx));
const label = el.querySelector(".hero-slide-label");
if (label)
label.textContent = String(idx + 1);
// Update input/textarea IDs to match new index
el.querySelectorAll("input, textarea").forEach((input) => {
if (!input.id)
return;
const newId = input.id.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
input.id = newId;
});
// Update upload button target-input so upload goes to correct slide
el.querySelectorAll(".btn-upload-image").forEach((btn) => {
const target = btn.getAttribute("data-target-input") || "";
if (!target)
return;
const newTarget = target.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
btn.setAttribute("data-target-input", newTarget);
});
});
};
addBtn.addEventListener("click", () => {
const template = container.querySelector(".hero-slide-item");
if (!template)
return;
const clone = template.cloneNode(true);
// Clear values for the cloned slide (IDs will be fixed by updateLabels)
clone.querySelectorAll("input, textarea").forEach((input) => {
input.value = "";
});
// Clear image previews
clone.querySelectorAll("img").forEach((img) => {
img.classList.add("d-none");
img.removeAttribute("src");
});
container.appendChild(clone);
updateLabels();
});
container.addEventListener("click", (e) => {
const btn = e.target.closest(".btn-remove-hero-slide");
if (!btn)
return;
const card = btn.closest(".hero-slide-item");
if (!card)
return;
const all = container.querySelectorAll(".hero-slide-item");
if (all.length <= 1) { // Không cho xóa slide cuối cùng
return;
}
card.remove();
updateLabels();
});
// Initial normalization (in case indices rendered from server are not 0..n)
updateLabels();
});
</script>

View File

@@ -1,150 +0,0 @@
<!-- Partners Tab -->
<div class="tab-pane fade" id="partners" role="tabpanel">
<div class="row g-4">
<!-- Visa Consultancy Awards -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-award me-2"></i>Awards & Certifications (Fixed 4 Items)
</h6>
</div>
<div class="card-body">
<div id="visaConsultancyContainer">
<% for(let i=0; i<4; i++) {
const item = (data.partners?.visaConsultancy?.items && data.partners.visaConsultancy.items[i]) || {};
%>
<div class="card mb-3 bg-light border visa-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="card-title fw-bold mb-0">Award #<%= i + 1 %></h6>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label fw-medium">Award Name</label>
<input type="text" class="form-control visa-name" value="<%= item.name || '' %>" placeholder="Award Name" />
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Year</label>
<input type="text" class="form-control visa-year" value="<%= item.year || '' %>" placeholder="e.g., 2025" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Icon / Logo</label>
<div class="input-group">
<input type="text" class="form-control visa-icon" id="visaIcon_<%= i %>" value="<%= item.icon || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="visaIcon_<%= i %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="mt-2 preview-container">
<img src="<%= item.icon || '' %>" class="img-thumbnail <%= item.icon ? '' : 'd-none' %>" style="height: 60px; object-fit: contain;">
</div>
</div>
</div>
</div>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Brand Logos -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-images me-2"></i>Brand Partner Logos (Slider)
</h6>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addBrandPartnerItem()">
<i class="fas fa-plus me-1"></i>Add Logo
</button>
</div>
<div class="card-body">
<div id="brandPartnersContainer" class="row g-3">
<% (data.partners?.brands?.items || []).forEach(function(item, index) { %>
<div class="col-md-4 brand-partner-item">
<div class="card border bg-light h-100">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small fw-bold">Brand Logo</span>
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="brandLogo_<%= index %>" value="<%= item.logo || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="brandLogo_<%= index %>" data-image-type="home">
<i class="fas fa-upload"></i>
</button>
</div>
<div class="mt-2 text-center preview-container">
<img src="<%= item.logo || '' %>" class="img-thumbnail <%= item.logo ? '' : 'd-none' %>" style="height: 50px; object-fit: contain;">
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Thu thập dữ liệu partners
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.partners = function() {
const visaItems = [];
document.querySelectorAll('.visa-item').forEach(el => {
visaItems.push({
name: el.querySelector('.visa-name').value,
year: el.querySelector('.visa-year').value,
icon: el.querySelector('.visa-icon').value
});
});
const brandItems = [];
document.querySelectorAll('.brand-partner-item').forEach(el => {
const logo = el.querySelector('.brand-logo-input').value;
if (logo) brandItems.push({ logo: logo });
});
return {
visaConsultancy: { items: visaItems },
brands: { items: brandItems }
};
};
function addBrandPartnerItem() {
const container = document.getElementById('brandPartnersContainer');
const id = 'brandLogo_' + Date.now();
const div = document.createElement('div');
div.className = 'col-md-4 brand-partner-item';
div.innerHTML = `
<div class="card border bg-light h-100">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small fw-bold">New Brand Logo</span>
<button type="button" class="btn btn-link text-danger p-0" onclick="this.closest('.brand-partner-item').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="input-group input-group-sm">
<input type="text" class="form-control brand-logo-input" id="${id}">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="${id}" data-image-type="home">
<i class="fas fa-upload"></i>
</button>
</div>
<div class="mt-2 text-center preview-container">
<img src="" class="img-thumbnail d-none" style="height: 50px; object-fit: contain;">
</div>
</div>
</div>
`;
container.appendChild(div);
}
</script>

View File

@@ -1,211 +0,0 @@
<!-- Testimonials Tab -->
<div class="tab-pane fade" id="testimonials" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>" placeholder="e.g., Student Reviews & Testimonials" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="testimonialsSubheading"
value="<%= data.testimonials?.subheading || '' %>" placeholder="e.g., What Our Students Say" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video URL</label>
<input type="text" class="form-control" id="testimonialsVideoUrl"
value="<%= data.testimonials?.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video Thumbnail</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="testimonialsVideoThumbnail"
value="<%= data.testimonials?.videoThumbnail || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsVideoThumbnail" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Testimonial Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-comments me-2"></i>Testimonials
</h6>
<button type="button" class="btn btn-sm btn-primary" id="addTestimonialBtn">
<i class="fas fa-plus me-1"></i>Add Testimonial
</button>
</div>
<div class="card-body" id="testimonialsItemsContainer">
<% (data.testimonials?.items || []).forEach(function(item, index) { %>
<div class="card mb-3 bg-light border testimonial-item" data-index="<%= index %>">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h6 class="card-title fw-bold mb-0">Testimonial <span class="testimonial-label">
<%= index + 1 %>
</span></h6>
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-testimonial">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Name</label>
<input type="text" class="form-control" id="testimonialsName_<%= index %>"
value="<%= item.name || '' %>" placeholder="e.g., Sohel Tanvir" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Role</label>
<input type="text" class="form-control" id="testimonialsRole_<%= index %>"
value="<%= item.role || '' %>" placeholder="e.g., Student" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Country</label>
<input type="text" class="form-control" id="testimonialsCountry_<%= index %>"
value="<%= item.country || '' %>" placeholder="e.g., Canada" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Rating</label>
<input type="number" class="form-control" id="testimonialsRating_<%= index %>"
value="<%= item.rating || 5 %>" min="1" max="5" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Comment</label>
<textarea class="form-control" id="testimonialsComment_<%= index %>" rows="3"
placeholder="Enter testimonial comment"><%= item.comment || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Avatar</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="testimonialsAvatar_<%= index %>"
value="<%= item.avatar || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsAvatar_<%= index %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect Testimonials data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.testimonials = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
const items = [];
document.querySelectorAll(".testimonial-item").forEach((el, idx) => {
const index = el.getAttribute("data-index") || idx;
const name = getVal(`testimonialsName_${index}`);
const role = getVal(`testimonialsRole_${index}`);
const country = getVal(`testimonialsCountry_${index}`);
const ratingStr = getVal(`testimonialsRating_${index}`);
const rating = ratingStr ? parseInt(ratingStr, 10) : 5;
const comment = getVal(`testimonialsComment_${index}`);
const avatar = getVal(`testimonialsAvatar_${index}`);
if (name || role || country || comment || avatar) {
items.push({
name,
role,
country,
rating: isNaN(rating) ? 5 : Math.min(Math.max(rating, 1), 5),
comment,
avatar,
});
}
});
return {
heading: getVal("testimonialsHeading"),
subheading: getVal("testimonialsSubheading"),
videoUrl: getVal("testimonialsVideoUrl"),
videoThumbnail: getVal("testimonialsVideoThumbnail"),
items,
};
};
// Client-side add/remove for testimonials
document.addEventListener("DOMContentLoaded", function () {
const container = document.getElementById("testimonialsItemsContainer");
const addBtn = document.getElementById("addTestimonialBtn");
if (!container || !addBtn) return;
const updateLabels = () => {
container.querySelectorAll(".testimonial-item").forEach((el, idx) => {
el.setAttribute("data-index", String(idx));
const label = el.querySelector(".testimonial-label");
if (label) label.textContent = String(idx + 1);
el.querySelectorAll("input, textarea").forEach((input) => {
if (!input.id) return;
const newId = input.id.replace(/testimonials\w+_\d+/, (match) => {
const [prefix] = match.split("_");
return `${prefix}_${idx}`;
});
input.id = newId;
});
});
};
addBtn.addEventListener("click", () => {
const template = container.querySelector(".testimonial-item");
if (!template) return;
const clone = template.cloneNode(true);
clone.querySelectorAll("input, textarea").forEach((input) => {
input.value = "";
});
container.appendChild(clone);
updateLabels();
});
container.addEventListener("click", (e) => {
const btn = e.target.closest(".btn-remove-testimonial");
if (!btn) return;
const card = btn.closest(".testimonial-item");
if (!card) return;
const all = container.querySelectorAll(".testimonial-item");
if (all.length <= 1) {
return;
}
card.remove();
updateLabels();
});
updateLabels();
});
</script>

View File

@@ -1,59 +0,0 @@
<!-- Video Gallery Tab -->
<div class="tab-pane fade" id="videogallery" role="tabpanel">
<div class="row g-4">
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-video me-2"></i>Video Gallery
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="videoGalleryHeading"
value="<%= data.videoGallery?.heading || '' %>" placeholder="e.g., VIDEO PLAY GALLERY" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Video URL</label>
<input type="text" class="form-control" id="videoGalleryVideoUrl"
value="<%= data.videoGallery?.videoUrl || '' %>" placeholder="https://example.com/video.mp4" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Thumbnail Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="videoGalleryThumbnail"
value="<%= data.videoGallery?.thumbnail || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="videoGalleryThumbnail" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.videoGallery?.thumbnail) { %>
<div class="mt-2">
<img src="<%= data.videoGallery.thumbnail %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Thumbnail preview" />
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect Video Gallery data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.videoGallery = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
return {
heading: getVal("videoGalleryHeading"),
videoUrl: getVal("videoGalleryVideoUrl"),
thumbnail: getVal("videoGalleryThumbnail"),
};
};
</script>

View File

@@ -1,145 +0,0 @@
<!-- Visa Countries Tab -->
<div class="tab-pane fade" id="visacountries" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="visaCountriesHeading"
value="<%= data.visaCountries?.heading || '' %>" placeholder="e.g., Visa & VISAWAY Services To UK" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="visaCountriesSubheading"
value="<%= data.visaCountries?.subheading || '' %>" placeholder="e.g., UK. United Kingdom" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="visaCountriesDescription" rows="3"
placeholder="Enter description"><%= data.visaCountries?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Countries (Featured Country configuration) -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-globe me-2"></i>Featured Country
</h6>
<small class="text-muted">This country is used in the home page feature section.</small>
</div>
<div class="card-body">
<% const featured=(data.visaCountries && Array.isArray(data.visaCountries.countries) &&
data.visaCountries.countries.length> 0)
? data.visaCountries.countries[0]
: {};
%>
<div class="card mb-3 bg-light border visa-country-item" data-index="0">
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Country Name</label>
<input type="text" class="form-control" id="visaCountriesName_0" value="<%= featured.name || '' %>"
placeholder="e.g., United Kingdom" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Country Code</label>
<input type="text" class="form-control" id="visaCountriesCode_0" value="<%= featured.code || '' %>"
placeholder="e.g., UK" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Flag / Illustration Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="visaCountriesFlag_0"
value="<%= featured.flag || '' %>" placeholder="/assets/img/home-1/feature/shape.png" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="visaCountriesFlag_0" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaCountriesLink_0" value="<%= featured.link || '' %>"
placeholder="/country-details/uk" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Visa Types (comma-separated)</label>
<textarea class="form-control" id="visaCountriesVisaTypes_0" rows="2"
placeholder="e.g., Student Visa, Work Visa, Tourist Visa"><%= (featured.visaTypes || []).join(', ') %></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="visaCountriesCtaLabel"
value="<%= data.visaCountries?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaCountriesCtaHref"
value="<%= data.visaCountries?.ctaButton?.href || '' %>" placeholder="/contact" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect Visa Countries data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.visaCountries = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
const visaTypesRaw = getVal("visaCountriesVisaTypes_0");
const visaTypes = visaTypesRaw ? visaTypesRaw.split(",").map((s) => s.trim()).filter(Boolean) : [];
const featuredCountry = {
name: getVal("visaCountriesName_0"),
code: getVal("visaCountriesCode_0"),
flag: getVal("visaCountriesFlag_0"),
link: getVal("visaCountriesLink_0"),
visaTypes
};
return {
heading: getVal("visaCountriesHeading"),
subheading: getVal("visaCountriesSubheading"),
description: getVal("visaCountriesDescription"),
countries: [featuredCountry],
ctaButton: {
label: getVal("visaCountriesCtaLabel"),
href: getVal("visaCountriesCtaHref")
}
};
};
</script>

View File

@@ -1,137 +0,0 @@
<!-- Visa Solutions Tab -->
<div class="tab-pane fade" id="visasolutions" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="visaSolutionsHeading"
value="<%= data.visaSolutions?.heading || '' %>" placeholder="e.g., Comprehensive Visa Solutions" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="visaSolutionsSubheading"
value="<%= data.visaSolutions?.subheading || '' %>" placeholder="e.g., Our Expert Services" />
</div>
</div>
</div>
</div>
</div>
<!-- Services Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="fas fa-list-ul me-2"></i>Visa Solutions Items
</h6>
</div>
<div class="card-body" id="visaSolutionsItemsContainer">
<% const vsItems=(data.visaSolutions && Array.isArray(data.visaSolutions.items) &&
data.visaSolutions.items.length===4) ? data.visaSolutions.items : (data.visaSolutions &&
Array.isArray(data.visaSolutions.items) && data.visaSolutions.items.length> 0)
? (function () {
const clone = data.visaSolutions.items.slice(0, 4);
while (clone.length < 4) clone.push({}); return clone; })() : [{}, {}, {}, {}]; %>
<% vsItems.forEach(function(item, index) { %>
<div class="card mb-3 bg-light border visa-solution-item" data-index="<%= index %>">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h6 class="card-title fw-bold mb-0">Service <span class="visa-solution-label">
<%= index + 1 %>
</span></h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label fw-medium">Number</label>
<input type="text" class="form-control" id="visaSolutionsNumber_<%= index %>"
value="<%= item.number || '' %>" placeholder="e.g., 01" />
</div>
<div class="col-md-9">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="visaSolutionsTitle_<%= index %>"
value="<%= item.title || '' %>" placeholder="e.g., Student Visa Guidance" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="visaSolutionsDescription_<%= index %>" rows="2"
placeholder="Enter description"><%= item.description || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="visaSolutionsLink_<%= index %>"
value="<%= item.link || '' %>" placeholder="/service-details" />
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect Visa Solutions data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.visaSolutions = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
const items = [];
document.querySelectorAll(".visa-solution-item").forEach((el, idx) => {
const index = el.getAttribute("data-index") || idx;
const number = getVal(`visaSolutionsNumber_${index}`);
const title = getVal(`visaSolutionsTitle_${index}`);
const description = getVal(`visaSolutionsDescription_${index}`);
const link = getVal(`visaSolutionsLink_${index}`);
if (number || title || description || link) {
items.push({ number, title, description, link });
}
});
return {
heading: getVal("visaSolutionsHeading"),
subheading: getVal("visaSolutionsSubheading"),
items,
};
};
// Client-side add/remove for visa solutions
document.addEventListener("DOMContentLoaded", function () {
const container = document.getElementById("visaSolutionsItemsContainer");
if (!container) return;
const updateLabels = () => {
const items = container.querySelectorAll(".visa-solution-item");
items.forEach((el, idx) => {
el.setAttribute("data-index", String(idx));
const label = el.querySelector(".visa-solution-label");
if (label) label.textContent = String(idx + 1);
el.querySelectorAll("input, textarea").forEach((input) => {
if (!input.id) return;
const newId = input.id.replace(/visaSolutions\w+_\d+/, (match) => {
const [prefix] = match.split("_");
return `${prefix}_${idx}`;
});
input.id = newId;
});
});
};
updateLabels();
});
</script>

View File

@@ -1,233 +0,0 @@
<!-- Why Choose Us Tab -->
<div class="tab-pane fade" id="whychooseus" role="tabpanel">
<div class="row g-4">
<!-- Basic Info -->
<div class="col-md-12">
<div class="card border shadow-sm mb-3">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2"></i>Basic Information
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="whyChooseUsHeading"
value="<%= data.whyChooseUs?.heading || '' %>"
placeholder="e.g., Turning Study Abroad Dreams Into Reality" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" id="whyChooseUsSubheading"
value="<%= data.whyChooseUs?.subheading || '' %>" placeholder="e.g., About Our Consultancy" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Highlight Word (Optional)</label>
<input type="text" class="form-control" id="whyChooseUsHighlightWord"
value="<%= data.whyChooseUs?.highlightWord || '' %>" placeholder="e.g., Dreams" />
<small class="text-muted">This word in the heading will be wrapped in a colored span.</small>
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="whyChooseUsDescription" rows="3"
placeholder="Enter description"><%= data.whyChooseUs?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- About Images -->
<div class="col-md-12">
<div class="card border shadow-sm mb-3">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-images me-2"></i>About Images (Left side)
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Main Image</label>
<small class="text-muted d-block mb-1">Recommended size: 375x419px</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsMainImage"
value="<%= data.whyChooseUs?.mainImage || '' %>" placeholder="/assets/img/home-1/about/about-1.jpg" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsMainImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.whyChooseUs?.mainImage) { %>
<div class="mt-2">
<img src="<%= data.whyChooseUs.mainImage %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Main about image preview" />
</div>
<% } %>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Secondary Image</label>
<small class="text-muted d-block mb-1">Recommended size: 376x394px</small>
<div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsSecondaryImage"
value="<%= data.whyChooseUs?.secondaryImage || '' %>"
placeholder="/assets/img/home-1/about/about-02.jpg" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsSecondaryImage" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.whyChooseUs?.secondaryImage) { %>
<div class="mt-2">
<img src="<%= data.whyChooseUs.secondaryImage %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Secondary about image preview" />
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
<!-- Items -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-list-ul me-2"></i>Why Choose Us Items
</h6>
</div>
<div class="card-body">
<% (data.whyChooseUs?.items || []).forEach(function(item, index) { %>
<div class="card mb-3 bg-light border">
<div class="card-body">
<h6 class="card-title fw-bold mb-3">Item <%= index + 1 %>
</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Icon URL</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="whyChooseUsIcon_<%= index %>"
value="<%= item.icon || '' %>" />
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="whyChooseUsIcon_<%= index %>" data-image-type="home">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="whyChooseUsTitle_<%= index %>"
value="<%= item.title || '' %>" placeholder="e.g., Global Reach" />
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<input type="text" class="form-control" id="whyChooseUsItemDescription_<%= index %>"
value="<%= item.description || '' %>" placeholder="e.g., Expanding Opportunities Worldwide" />
</div>
</div>
</div>
</div>
<% }); %>
</div>
</div>
</div>
<!-- Features -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-star me-2"></i>Features
</h6>
</div>
<div class="card-body">
<% (data.whyChooseUs?.features || []).forEach(function(feature, index) { %>
<div class="mb-3">
<label class="form-label fw-medium">Feature <%= index + 1 %></label>
<input type="text" class="form-control" id="whyChooseUsFeature_<%= index %>" value="<%= feature %>"
placeholder="Enter feature" />
</div>
<% }); %>
</div>
</div>
</div>
<!-- CTA Button -->
<div class="col-md-12">
<div class="card border shadow-sm">
<div class="card-header bg-white">
<h6 class="mb-0">
<i class="fas fa-mouse-pointer me-2"></i>CTA Button
</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Label</label>
<input type="text" class="form-control" id="whyChooseUsCtaLabel"
value="<%= data.whyChooseUs?.ctaButton?.label || '' %>" placeholder="e.g., Get Started" />
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link</label>
<input type="text" class="form-control" id="whyChooseUsCtaHref"
value="<%= data.whyChooseUs?.ctaButton?.href || '' %>" placeholder="/about" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Register scraper to collect Why Choose Us data before form submit
window.homeScrapers = window.homeScrapers || {};
window.homeScrapers.whyChooseUs = () => {
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
// Collect items
const items = [];
document.querySelectorAll('input[id^="whyChooseUsTitle_"]').forEach((titleInput) => {
const id = titleInput.id || "";
const match = id.match(/whyChooseUsTitle_(\d+)/);
if (!match) return;
const idx = match[1];
const title = (titleInput.value || "").trim();
const icon = getVal(`whyChooseUsIcon_${idx}`);
const description = getVal(`whyChooseUsItemDescription_${idx}`);
// Bỏ qua item trống hoàn toàn
if (title || icon || description) {
items.push({ icon, title, description });
}
});
// Collect features
const features = [];
document.querySelectorAll('input[id^="whyChooseUsFeature_"]').forEach((featureInput) => {
const value = (featureInput.value || "").trim();
if (value) {
features.push(value);
}
});
return {
heading: getVal("whyChooseUsHeading"),
subheading: getVal("whyChooseUsSubheading"),
description: getVal("whyChooseUsDescription"),
highlightWord: getVal("whyChooseUsHighlightWord"),
mainImage: getVal("whyChooseUsMainImage"),
secondaryImage: getVal("whyChooseUsSecondaryImage"),
items,
features,
ctaButton: {
label: getVal("whyChooseUsCtaLabel"),
href: getVal("whyChooseUsCtaHref"),
},
};
};
</script>

View File

@@ -1,438 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Manage testimonials section</p>
</div>
<div>
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Homepage
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="testimonialForm"
action=" /update">
<!-- Hidden input for items JSON data -->
<input type="hidden" name="items" id="itemsJson">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-quote-left me-2"></i>Testimonials Settings</h5>
</div>
<div class="card-body">
<!-- Header Section -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" name="heading" id="heading"
value="<%= data.heading || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subheading</label>
<input type="text" class="form-control" name="subheading" id="subheading"
value="<%= data.subheading || '' %>">
</div>
</div>
<!-- Video Section -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Video URL</label>
<input type="text" class="form-control" name="videoUrl" id="videoUrl"
value="<%= data.videoUrl || '' %>"
placeholder="https://www.youtube.com/watch?v=...">
<small class="text-muted">YouTube video URL</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Video Thumbnail</label>
<div class="input-group">
<input type="text" class="form-control" name="videoThumbnail" id="videoThumbnail"
value="<%= data.videoThumbnail || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="videoThumbnail" data-image-type="testimonial">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (data.videoThumbnail) { %>
<img src="<%= data.videoThumbnail %>" class="img-thumbnail mt-2"
style="max-height: 100px;" alt="Thumbnail preview">
<% } %>
</div>
</div>
<hr class="my-4">
<!-- Testimonial Items -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Testimonial Items</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addTestimonialItem()">
<i class="fas fa-plus"></i> Add Testimonial
</button>
</div>
<div id="testimonialItemsContainer">
<% if (data.items && data.items.length> 0) { %>
<% data.items.forEach((item, index)=> { %>
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="itemName_<%= index %>"
value="<%= item.name || '' %>">
</div>
<div class="col-md-3">
<label class="form-label">Role</label>
<input type="text" class="form-control" name="itemRole_<%= index %>"
value="<%= item.role || '' %>">
</div>
<div class="col-md-3">
<label class="form-label">Country</label>
<input type="text" class="form-control"
name="itemCountry_<%= index %>"
value="<%= item.country || '' %>">
</div>
<div class="col-md-3">
<label class="form-label">Rating (1-5)</label>
<select class="form-select" name="itemRating_<%= index %>">
<% for (let i=1; i <=5; i++) { %>
<option value="<%= i %>" <%=item.rating===i ? 'selected'
: '' %>>
<%= i %> Star<%= i> 1 ? 's' : '' %>
</option>
<% } %>
</select>
</div>
<div class="col-md-8">
<label class="form-label">Comment</label>
<textarea class="form-control" name="itemComment_<%= index %>"
rows="3"><%= item.comment || '' %></textarea>
</div>
<div class="col-md-4">
<label class="form-label">Avatar</label>
<div class="input-group">
<input type="text" class="form-control"
name="itemAvatar_<%= index %>"
value="<%= item.avatar || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="itemAvatar_<%= index %>"
data-image-type="testimonial">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<% if (item.avatar) { %>
<img src="<%= item.avatar %>"
class="img-thumbnail mt-2 avatar-preview"
style="max-height: 60px; max-width: 60px; border-radius: 50%;"
alt="Avatar">
<% } %>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
onclick="removeTestimonialItem(this)">
<i class="fas fa-trash me-2"></i>Remove
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="testimonialDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('testimonialDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = { items: [] };
}
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('testimonialForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateItemsJson();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
}
function updateItemsJson() {
const items = [];
document.querySelectorAll('.testimonial-item').forEach((item, index) => {
const name = item.querySelector(`[name="itemName_${index}"]`)?.value ||
item.querySelector('[name^="itemName_"]')?.value || '';
const role = item.querySelector(`[name="itemRole_${index}"]`)?.value ||
item.querySelector('[name^="itemRole_"]')?.value || '';
const country = item.querySelector(`[name="itemCountry_${index}"]`)?.value ||
item.querySelector('[name^="itemCountry_"]')?.value || '';
const rating = parseInt(item.querySelector(`[name="itemRating_${index}"]`)?.value ||
item.querySelector('[name^="itemRating_"]')?.value || '5');
const comment = item.querySelector(`[name="itemComment_${index}"]`)?.value ||
item.querySelector('[name^="itemComment_"]')?.value || '';
const avatar = item.querySelector(`[name="itemAvatar_${index}"]`)?.value ||
item.querySelector('[name^="itemAvatar_"]')?.value || '';
items.push({ name, role, country, rating, comment, avatar });
});
document.getElementById('itemsJson').value = JSON.stringify(items);
}
function addTestimonialItem() {
const container = document.getElementById('testimonialItemsContainer');
const index = container.querySelectorAll('.testimonial-item').length;
const html = `
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="itemName_${index}" value="">
</div>
<div class="col-md-3">
<label class="form-label">Role</label>
<input type="text" class="form-control" name="itemRole_${index}" value="">
</div>
<div class="col-md-3">
<label class="form-label">Country</label>
<input type="text" class="form-control" name="itemCountry_${index}" value="">
</div>
<div class="col-md-3">
<label class="form-label">Rating (1-5)</label>
<select class="form-select" name="itemRating_${index}">
<option value="1">1 Star</option>
<option value="2">2 Stars</option>
<option value="3">3 Stars</option>
<option value="4">4 Stars</option>
<option value="5" selected>5 Stars</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label">Comment</label>
<textarea class="form-control" name="itemComment_${index}" rows="3"></textarea>
</div>
<div class="col-md-4">
<label class="form-label">Avatar</label>
<div class="input-group">
<input type="text" class="form-control" name="itemAvatar_${index}" value="">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="itemAvatar_${index}" data-image-type="testimonial">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
onclick="removeTestimonialItem(this)">
<i class="fas fa-trash me-2"></i>Remove
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
// Re-attach upload handlers for new item
container.querySelector('.testimonial-item:last-child .btn-upload-image').addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
}
function removeTestimonialItem(button) {
if (confirm('Are you sure you want to remove this testimonial?')) {
button.closest('.testimonial-item').remove();
reindexItems();
}
}
function reindexItems() {
document.querySelectorAll('.testimonial-item').forEach((item, index) => {
item.querySelectorAll('[name^="item"]').forEach(input => {
const baseName = input.name.replace(/_\d+$/, '');
input.name = `${baseName}_${index}`;
});
});
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
function openImageUploader(targetInput, imageType) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
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');
}
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
if (!input) {
throw new Error('Target input not found');
}
input.value = result.path;
// Update preview if exists
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://')))
? result.path
: (window.location.origin + result.path);
const parent = input.closest('.col-md-4') || input.closest('div');
let previewImg = parent.querySelector('.avatar-preview, .img-thumbnail');
if (previewImg) {
previewImg.src = previewUrl;
} else {
const img = document.createElement('img');
img.src = previewUrl;
img.className = 'img-thumbnail mt-2';
img.style.maxHeight = '60px';
img.alt = 'Preview';
input.closest('.input-group').after(img);
}
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
showToast('Success', 'Image uploaded successfully', 'success');
} catch (error) {
console.error('Upload error:', error);
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
}
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}:</strong> ${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
}
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, {
animation: true,
autohide: true,
delay: 3000
});
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
</script>

View File

@@ -1,206 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Manage video gallery section on homepage</p>
</div>
<div>
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Homepage
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="videoGalleryForm"
action="/admin/video-gallery/update">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-video me-2"></i>Video Gallery Settings</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" name="heading" id="heading"
value="<%= data.heading || '' %>" placeholder="e.g., Explore World">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Video URL</label>
<div class="input-group">
<input type="text" class="form-control" name="videoUrl" id="videoUrl"
value="<%= data.videoUrl || '' %>" placeholder="/assets/video/sample.mp4">
<button type="button" class="btn btn-outline-primary btn-upload-video"
data-target-input="videoUrl">
<i class="fas fa-upload me-1"></i>Upload Video
</button>
</div>
<small class="text-muted">MP4 video file path or URL</small>
<% if (data.videoUrl) { %>
<div class="mt-2">
<video width="100%" height="200" controls style="border-radius: 8px;">
<source src="<%= data.videoUrl %>" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
initializeFormHandlers();
});
function initializeFormHandlers() {
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
document.querySelectorAll('.btn-upload-video').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
openVideoUploader(targetInput);
});
});
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
function openImageUploader(targetInput, imageType) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
document.getElementById(targetInput).value = result.path;
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
alert('Image uploaded successfully!');
location.reload();
} catch (error) {
console.error('Upload error:', error);
alert('Failed to upload image: ' + error.message);
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
function openVideoUploader(targetInput) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'video/mp4,video/webm,video/ogg';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('video', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch('/admin/upload/video', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Upload failed');
}
document.getElementById(targetInput).value = result.path;
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
alert('Video uploaded successfully!');
location.reload();
} catch (error) {
console.error('Upload error:', error);
alert('Failed to upload video: ' + error.message);
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
</script>

View File

@@ -1,638 +0,0 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
<%= title %>
</h1>
<p class="text-muted mb-0">
Manage insurance page content with hero section, page information, and rich content editor
</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-primary preview-btn">
<i class="fas fa-eye me-2"></i>Preview
</button>
<button type="submit" form="insuranceForm" class="btn btn-primary" id="saveBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</div>
<form id="insuranceForm" action="/admin/insurance/update" method="POST" class="needs-validation" novalidate>
<!-- Hidden inputs -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="page" id="pageJson">
<input type="hidden" name="content" id="contentJson">
<div class="row">
<!-- Main Content Column -->
<div class="col-lg-8">
<!-- Page Information Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Page Title <span class="text-danger">*</span></label>
<textarea class="form-control" id="pageTitle" rows="2" required><%= data.page?.title || 'Insurance & Travel Information' %></textarea>
<div class="invalid-feedback">Please enter a page title</div>
</div>
</div>
</div>
<!-- Hero Section Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Hero Section</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-5">
<div class="mb-3">
<label class="form-label">Background Image</label>
<div class="input-group">
<input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '/uploads/banner/b13.jpg' %>" readonly>
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="insurance">
<i class="fas fa-upload"></i>
</button>
</div>
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
</div>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px; width: 100%;">
<% if (data.hero?.backgroundImage) { %>
<%
const heroImgSrc = data.hero.backgroundImage.startsWith('http')
? data.hero.backgroundImage
: (frontendUrl ? frontendUrl + '/' + data.hero.backgroundImage : data.hero.backgroundImage);
%>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center; flex-direction: column;">
<i class="fas fa-image fa-3x mb-2"></i><p class="mb-0">Image preview not available</p>
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<i class="fas fa-image fa-3x mb-2"></i><p class="mb-0">No image selected</p>
</div>
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<label class="form-label">Hero Title</label>
<input type="text" class="form-control" id="heroTitle"
value="<%= data.hero?.title || 'Insurance & Travel Cancellation Guarantee' %>">
</div>
<div class="col-md-6">
<label class="form-label">Hero Subtitle</label>
<input type="text" class="form-control" id="heroSubtitle"
value="<%= data.hero?.subtitle || 'Comprehensive coverage for your peace of mind' %>">
</div>
</div>
</div>
</div>
<!-- Content Editor Section -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Editor</h5>
<p class="text-muted mb-0 small">Write your insurance page content just like a blog post</p>
</div>
<div class="card-body">
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
</div>
</div>
</div>
<!-- Sidebar Column -->
<div class="col-lg-4">
<!-- SEO Settings Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">SEO Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" class="form-control" id="metadataTitle"
value="<%= data.page?.metadata?.title || '' %>">
<small class="text-muted">Title for search engines (max 60 characters)</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea class="form-control" id="metadataDescription" rows="3"
placeholder="Meta description for SEO"><%= data.page?.metadata?.description || '' %></textarea>
<small class="text-muted">Description for search engines (max 160 characters)</small>
</div>
</div>
</div>
<!-- Page Settings Card -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pageDivider"
<%= data.page?.divider !== false ? 'checked' : '' %>>
<label class="form-check-label" for="pageDivider">Show page divider</label>
</div>
<small class="text-muted">Display divider line below page title</small>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enableScrollspy"
<%= data.hero?.enableScrollspy ? 'checked' : '' %>>
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
</div>
<small class="text-muted">Creates table of contents from headers</small>
</div>
</div>
</div>
<!-- Content Tips Card -->
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Tips</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Insurance Content:</h6>
<ul class="mb-0 small">
<li>Use <strong>Header 2</strong> for main sections</li>
<li>Use <strong>Header 3</strong> for subsections</li>
<li>Use <strong>Lists</strong> for coverage items</li>
<li>Use <strong>Quote</strong> for important notes</li>
<li>Use <strong>Embed</strong> for video explanations</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Insurance Page Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<iframe id="previewFrame" style="width: 100%; height: 80vh; border: none;"></iframe>
</div>
</div>
</div>
</div>
<!-- Hidden inputs for file upload -->
<input type="file" id="directImageUpload" style="display: none;">
<input type="hidden" id="currentImageType">
<input type="hidden" id="currentTargetInput">
<!-- Editor.js Dependencies -->
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
<script>
// Insurance Content Manager
class InsuranceContentManager {
// Convert Editor.js data to Insurance content structure
convertEditorToInsurance(editorData) {
const contentItems = [];
editorData.blocks.forEach(block => {
switch (block.type) {
case 'header':
contentItems.push({
type: 'header',
level: block.data.level || 2,
text: block.data.text || ''
});
break;
case 'paragraph':
contentItems.push({
type: 'paragraph',
text: block.data.text || ''
});
break;
case 'list':
contentItems.push({
type: 'list',
level: block.data.style === 'ordered' ? 1 : 0, // 1 for ordered, 0 for unordered
items: block.data.items || []
});
break;
case 'quote':
contentItems.push({
type: 'note',
text: block.data.text || '',
caption: block.data.caption || ''
});
break;
case 'embed':
contentItems.push({
type: 'embed',
source: block.data.service || 'youtube',
url: block.data.embed || '',
embed: block.data.embed || '',
caption: block.data.caption || '',
width: block.data.width || 560,
height: block.data.height || 315
});
break;
}
});
return contentItems;
}
// Convert Insurance content to Editor.js data
convertInsuranceToEditor(insuranceContent) {
const blocks = [];
insuranceContent.forEach(item => {
switch (item.type) {
case 'header':
blocks.push({
type: 'header',
data: {
text: item.text || '',
level: item.level || 2
}
});
break;
case 'paragraph':
blocks.push({
type: 'paragraph',
data: {
text: item.text || ''
}
});
break;
case 'section':
if (item.title) {
blocks.push({
type: 'header',
data: {
text: item.title,
level: 3
}
});
}
if (item.content) {
blocks.push({
type: 'paragraph',
data: {
text: item.content
}
});
}
break;
case 'list':
blocks.push({
type: 'list',
data: {
style: item.level === 1 ? 'ordered' : 'unordered', // Check level to determine style
items: item.items || []
}
});
break;
case 'note':
blocks.push({
type: 'quote',
data: {
text: item.text || '',
caption: item.caption || ''
}
});
break;
case 'embed':
blocks.push({
type: 'embed',
data: {
service: item.source || 'youtube',
embed: item.url || item.embed || '',
caption: item.caption || '',
width: item.width || 560,
height: item.height || 315
}
});
break;
}
});
return {
time: Date.now(),
blocks: blocks,
version: "2.28.2"
};
}
}
// Initialize Editor
document.addEventListener('DOMContentLoaded', async () => {
// Initialize content manager
window.insuranceContentManager = new InsuranceContentManager();
// Load existing content if available
let initialEditorData = {
time: Date.now(),
blocks: [],
version: "2.28.2"
};
if (window.INSURANCE_DATA?.content?.content) {
try {
const insuranceContent = window.INSURANCE_DATA.content.content;
initialEditorData = window.insuranceContentManager.convertInsuranceToEditor(insuranceContent);
} catch (error) {
console.error('Error loading insurance content:', error);
}
}
// Initialize Editor via BlogEditor module (adds uploader + autosave)
let blogEditorInstance = null;
try {
if (window.BlogEditor) {
const initialCustom = window.INSURANCE_DATA?.content?.content ? { blocks: window.INSURANCE_DATA.content.content } : { blocks: [] };
blogEditorInstance = new window.BlogEditor('editorjs', initialCustom);
window.blogEditorInstance = blogEditorInstance;
console.log('BlogEditor instance created for insurance');
} else {
console.warn('BlogEditor not available. Falling back to EditorJS init.');
window.insuranceEditor = new EditorJS({
holder: 'editorjs',
data: initialEditorData,
tools: {
header: Header,
paragraph: {
class: Paragraph,
inlineToolbar: true
},
list: List,
quote: Quote,
marker: Marker,
embed: Embed
}
});
}
} catch (err) {
console.error('Error initializing editor module:', err);
window.insuranceEditor = new EditorJS({ holder: 'editorjs', data: initialEditorData });
}
// Form submission handler
const form = document.getElementById('insuranceForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
// Save editor content
let editorData = null;
if (window.blogEditorInstance?.editor) {
editorData = await window.blogEditorInstance.editor.save();
} else if (window.insuranceEditor) {
editorData = await window.insuranceEditor.save();
} else {
throw new Error('Editor not initialized');
}
// Convert to Insurance content structure
const insuranceContent = window.insuranceContentManager.convertEditorToInsurance(editorData);
// Prepare JSON data
const heroData = {
title: document.getElementById('heroTitle').value,
subtitle: document.getElementById('heroSubtitle').value,
backgroundImage: document.getElementById('heroBackgroundImage').value,
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
overlayStyle: { backgroundColor: "rgba(0, 0, 0, 0)" },
titleClass: "text-white text-[5vw] uk-text-center",
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
enableScrollspy: true
};
const pageData = {
title: document.getElementById('pageTitle').value,
divider: document.getElementById('pageDivider').checked,
sectionClass: "uk-section-default uk-section-overlap uk-section",
titleClass: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
dividerClass: "uk-divider-small uk-text-left@m uk-text-center"
};
const contentData = {
sectionClass: "uk-section-muted uk-section-overlap uk-section",
textClass: "uk-panel uk-margin text-[1vw]",
content: insuranceContent
};
// Set hidden inputs
document.getElementById('heroJson').value = JSON.stringify(heroData);
document.getElementById('pageJson').value = JSON.stringify(pageData);
document.getElementById('contentJson').value = JSON.stringify(contentData);
// Submit form
form.submit();
} catch (error) {
console.error('Error saving insurance content:', error);
alert('Error saving content. Please check console for details.');
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Preview functionality
const previewBtn = document.querySelector('.preview-btn');
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
previewBtn.addEventListener('click', async function () {
try {
// Save editor content first
let editorData = null;
if (window.blogEditorInstance?.editor) {
editorData = await window.blogEditorInstance.editor.save();
} else if (window.insuranceEditor) {
editorData = await window.insuranceEditor.save();
} else {
throw new Error('Editor not initialized');
}
const insuranceContent = window.insuranceContentManager.convertEditorToInsurance(editorData);
// Prepare data for preview
const heroData = {
title: document.getElementById('heroTitle').value,
subtitle: document.getElementById('heroSubtitle').value,
backgroundImage: document.getElementById('heroBackgroundImage').value
};
const pageData = {
title: document.getElementById('pageTitle').value,
divider: document.getElementById('pageDivider').checked
};
const contentData = {
content: insuranceContent
};
const response = await fetch('/admin/insurance/preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
hero: JSON.stringify(heroData),
page: JSON.stringify(pageData),
content: JSON.stringify(contentData)
})
});
const html = await response.text();
const previewFrame = document.getElementById('previewFrame');
previewFrame.srcdoc = html;
previewModal.show();
} catch (error) {
console.error('Error generating preview:', error);
alert('Error generating preview. Please try again.');
}
});
// Image upload handling
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');
document.getElementById('currentImageType').value = imageType;
document.getElementById('currentTargetInput').value = targetInput;
document.getElementById('directImageUpload').click();
});
});
// Handle file selection
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 {
showToast('Uploading image...', 'info');
const uploadResponse = await fetch(`/admin/upload/image?imageType=${imageType}`, {
method: 'POST',
body: formData
});
const uploadResult = await uploadResponse.json();
if (uploadResult.success && uploadResult.path) {
const inputElement = document.getElementById(targetInput);
if (inputElement) {
inputElement.value = uploadResult.path;
// Update preview
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(uploadResult.path);
}
// Show success message
showToast('Image uploaded successfully', 'success');
}
} else {
showToast(uploadResult.error || 'Error uploading image', 'error');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error uploading image', 'error');
}
this.value = '';
});
function updateHeroImagePreview(imageUrl) {
const previewDiv = document.getElementById('heroImagePreview');
let img = previewDiv.querySelector('img');
let fallback = previewDiv.querySelector('.border');
if (!img) {
img = document.createElement('img');
img.className = 'img-thumbnail';
img.style.height = '200px';
img.style.width = '100%';
img.style.objectFit = 'cover';
img.alt = 'Background image preview';
img.onerror = function() {
this.style.display = 'none';
if (fallback) fallback.style.display = 'flex';
};
previewDiv.insertBefore(img, fallback);
}
img.src = imageUrl;
img.style.display = 'block';
if (fallback) fallback.style.display = 'none';
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
toast.setAttribute('role', 'alert');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
});
// Store insurance data in global variable
window.INSURANCE_DATA = <%- JSON.stringify(data) %>;
</script>

View File

@@ -1,743 +1,110 @@
<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>
<!-- Page Header -->
<div class="page-title-area">
<div>
<h1>Level Management</h1>
<p class="subtitle">All degree levels registered in the system</p>
</div>
</div>
<div class="row g-3">
<!-- Add Form -->
<div class="col-lg-4">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-plus-circle"></i> Add Level</h5>
</div>
<div class="card-body">
<form method="POST" action="/admin/level/create">
<div class="mb-3">
<label class="form-label">Level Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="type" placeholder="e.g. Undergraduate, Postgraduate..." required>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-plus"></i> Add Level
</button>
</form>
</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
<!-- Table -->
<div class="col-lg-8">
<div class="card border-0">
<div class="card-header">
<h5 class="card-header-title"><i class="fas fa-layer-group"></i> Levels</h5>
<span class="badge badge-soft-primary"><%= levels ? levels.length : 0 %> total</span>
</div>
<div class="card-body p-0">
<% if (levels && levels.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:50px">#</th>
<th>Level Name</th>
<th style="width:130px">Actions</th>
</tr>
</thead>
<tbody>
<% levels.forEach((level, index) => { %>
<tr>
<td style="color:var(--text-muted)"><%= index + 1 %></td>
<td>
<span class="level-type-display-<%= level._id %>" style="font-weight:500"><%= level.type %></span>
<form method="POST" action="/admin/level/<%= level._id %>/edit"
class="level-edit-form-<%= level._id %> d-none d-flex gap-2 align-items-center">
<input type="text" class="form-control form-control-sm" name="type" value="<%= level.type %>" required>
<button type="submit" class="btn btn-sm btn-outline-primary btn-icon" title="Save">
<i class="fas fa-check"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-icon" title="Cancel"
onclick="cancelLevelEdit('<%= level._id %>')">
<i class="fas fa-times"></i>
</button>
</form>
</td>
<td>
<div class="table-actions">
<button type="button" class="btn btn-sm btn-outline-primary btn-icon" title="Edit"
onclick="startLevelEdit('<%= level._id %>')">
<i class="fas fa-pen"></i>
</button>
<form method="POST" action="/admin/level/<%= level._id %>/delete" style="display:inline;"
onsubmit="return confirm('Delete level?')">
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</form>
</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>
</td>
</tr>
<% }); %>
</tbody>
</table>
</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>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-layer-group"></i></div>
<h5>No levels yet</h5>
<p>Use the form on the left to add the first level.</p>
</div>
<% } %>
</div>
</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;
}
function startLevelEdit(id) {
document.querySelector('.level-type-display-' + id).classList.add('d-none');
const form = document.querySelector('.level-edit-form-' + id);
form.classList.remove('d-none');
form.classList.add('d-flex');
}
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 cancelLevelEdit(id) {
document.querySelector('.level-type-display-' + id).classList.remove('d-none');
const form = document.querySelector('.level-edit-form-' + id);
form.classList.add('d-none');
form.classList.remove('d-flex');
}
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>

View File

@@ -1,742 +0,0 @@
<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);">
<%= title %>
</h1>
<p class="text-muted mb-0">Edit content displayed on Pricing page</p>
</div>
<div>
<a href="<%= frontendUrl %>/pricing/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Pricing Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="pricingForm" action="/admin/pricing/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="pricingSection" id="pricingSectionJson">
<input type="hidden" name="plans" id="plansJson">
<input type="hidden" name="testimonials" id="testimonialsJson">
<!-- 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="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#pricingSection" role="tab">
<i class="fas fa-tags me-2"></i>Pricing Section
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#plans" role="tab">
<i class="fas fa-dollar-sign me-2"></i>Plans
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
<i class="fas fa-quote-right me-2"></i>Testimonials
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Hero Section</h6>
<div class="row g-3">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage"
data-image-type="pricing">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px;">
<% if (data.hero?.backgroundImage) { %>
<% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc &&
!heroImgSrc.startsWith('http://') &&
!heroImgSrc.startsWith('https://')) {
heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' +
heroImgSrc; } %>
<img src="<%= heroImgSrc %>" class="img-thumbnail"
id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Section Tab -->
<div class="tab-pane fade" id="pricingSection" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Pricing Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="pricingSectionSubtitle"
value="<%= data.pricingSection?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="pricingSectionHeading"
value="<%= data.pricingSection?.heading || '' %>">
</div>
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="pricingSectionDescription"
rows="3"><%= data.pricingSection?.description || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Plans Tab -->
<div class="tab-pane fade" id="plans" role="tabpanel">
<!-- Monthly Plans -->
<div class="card border shadow-sm mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Monthly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('monthly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="monthlyPlansContainer">
<% if (data.plans?.monthly && data.plans.monthly.length> 0) { %>
<% data.plans.monthly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="monthly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
<!-- Yearly Plans -->
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Yearly Plans</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addPlan('yearly')">
<i class="fas fa-plus"></i> Add Plan
</button>
</div>
<div id="yearlyPlansContainer">
<% if (data.plans?.yearly && data.plans.yearly.length> 0) { %>
<% data.plans.yearly.forEach((plan, index)=> { %>
<div class="card mb-3 plan-item" data-type="yearly">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name"
value="<%= plan.name || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price"
value="<%= plan.price || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text"
class="form-control plan-currency"
value="<%= plan.currency || '$' %>">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period"
value="<%= plan.period || 'mo' %>">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default"
<%=plan.style==='default' ? 'selected' : ''
%>>Default</option>
<option value="style-2"
<%=plan.style==='style-2' ? 'selected' : ''
%>>Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text"
class="form-control plan-button-text"
value="<%= plan.buttonText || 'Get Started Today' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text"
class="form-control plan-button-link"
value="<%= plan.buttonLink || '/pricing' %>">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text"
class="form-control plan-button-icon"
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per
line)</label>
<textarea class="form-control plan-features"
rows="4"><%= (plan.features || []).join('\n') %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Testimonials Tab -->
<div class="tab-pane fade" id="testimonials" role="tabpanel">
<div class="card border shadow-sm mb-4">
<div class="card-body">
<h6 class="fw-medium mb-3">Testimonials Section Header</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" id="testimonialsSubtitle"
value="<%= data.testimonials?.subtitle || '' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Heading</label>
<input type="text" class="form-control" id="testimonialsHeading"
value="<%= data.testimonials?.heading || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Text</label>
<input type="text" class="form-control" id="testimonialsButtonText"
value="<%= data.testimonials?.buttonText || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Button Link</label>
<input type="text" class="form-control" id="testimonialsButtonLink"
value="<%= data.testimonials?.buttonLink || '' %>">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Section Image</label>
<div class="input-group">
<input type="text" class="form-control" id="testimonialsImage"
value="<%= data.testimonials?.image || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="testimonialsImage" data-image-type="pricing">
<i class="fas fa-upload"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Testimonial Items</h6>
<button type="button" class="btn btn-primary btn-sm"
onclick="addTestimonial()">
<i class="fas fa-plus"></i> Add Testimonial
</button>
</div>
<div id="testimonialsContainer">
<% if (data.testimonials?.items && data.testimonials.items.length> 0) { %>
<% data.testimonials.items.forEach((item, index)=> { %>
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text"
class="form-control testimonial-name"
value="<%= item.name || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text"
class="form-control testimonial-role"
value="<%= item.role || '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1" <%=item.rating===1
? 'selected' : '' %>>1 Star</option>
<option value="2" <%=item.rating===2
? 'selected' : '' %>>2 Stars</option>
<option value="3" <%=item.rating===3
? 'selected' : '' %>>3 Stars</option>
<option value="4" <%=item.rating===4
? 'selected' : '' %>>4 Stars</option>
<option value="5" <%=item.rating===5
? 'selected' : '' %>>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content"
rows="3"><%= item.content || '' %></textarea>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="pricingDataJson"><%- JSON.stringify(data) %></script>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
try {
var jsonScript = document.getElementById('pricingDataJson');
originalFormData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.error('Error parsing originalFormData:', e);
originalFormData = {};
}
updateAllJsonInputs();
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('pricingForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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);
});
});
// Update preview when background image changes
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
updateHeroImagePreview(this.value);
});
}
function updateHeroImagePreview(imagePath) {
const previewContainer = document.getElementById('heroImagePreview');
if (imagePath) {
let imgSrc = imagePath;
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
}
previewContainer.innerHTML = `
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
style="height: 200px; width: 100%; object-fit: cover;"
alt="Background image preview"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
`;
} else {
previewContainer.innerHTML = `
<div class="border rounded p-5 text-center text-muted"
style="height: 200px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
`;
}
}
function updateAllJsonInputs() {
updateJsonData();
}
function updateJsonData() {
// Hero data
const heroData = {
title: document.getElementById('heroTitle').value || '',
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
shapeImage: originalFormData?.hero?.shapeImage || '/assets/img/inner-page/shape.png',
breadcrumb: originalFormData?.hero?.breadcrumb || [],
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Pricing Section data
const pricingSectionData = {
subtitle: document.getElementById('pricingSectionSubtitle').value || '',
heading: document.getElementById('pricingSectionHeading').value || '',
description: document.getElementById('pricingSectionDescription').value || '',
};
document.getElementById('pricingSectionJson').value = JSON.stringify(pricingSectionData);
// Plans data
const monthlyPlans = [];
document.querySelectorAll('#monthlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
monthlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
const yearlyPlans = [];
document.querySelectorAll('#yearlyPlansContainer .plan-item').forEach(item => {
const featuresText = item.querySelector('.plan-features').value || '';
yearlyPlans.push({
name: item.querySelector('.plan-name').value || '',
price: item.querySelector('.plan-price').value || '0',
currency: item.querySelector('.plan-currency').value || '$',
period: item.querySelector('.plan-period').value || 'mo',
style: item.querySelector('.plan-style').value || 'default',
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
features: featuresText.split('\n').filter(f => f.trim()),
});
});
document.getElementById('plansJson').value = JSON.stringify({
monthly: monthlyPlans,
yearly: yearlyPlans,
});
// Testimonials data
const testimonialItems = [];
document.querySelectorAll('#testimonialsContainer .testimonial-item').forEach(item => {
testimonialItems.push({
name: item.querySelector('.testimonial-name').value || '',
role: item.querySelector('.testimonial-role').value || '',
rating: parseInt(item.querySelector('.testimonial-rating').value) || 5,
content: item.querySelector('.testimonial-content').value || '',
});
});
const testimonialsData = {
subtitle: document.getElementById('testimonialsSubtitle').value || '',
heading: document.getElementById('testimonialsHeading').value || '',
buttonText: document.getElementById('testimonialsButtonText').value || '',
buttonLink: document.getElementById('testimonialsButtonLink').value || '',
buttonIcon: originalFormData?.testimonials?.buttonIcon || 'fa-solid fa-arrow-right',
image: document.getElementById('testimonialsImage').value || '',
items: testimonialItems,
};
document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData);
}
function addPlan(type) {
const container = document.getElementById(type + 'PlansContainer');
const html = `
<div class="card mb-3 plan-item" data-type="${type}">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Plan Name</label>
<input type="text" class="form-control plan-name" value="">
</div>
<div class="col-md-2">
<label class="form-label">Price</label>
<input type="text" class="form-control plan-price" value="">
</div>
<div class="col-md-2">
<label class="form-label">Currency</label>
<input type="text" class="form-control plan-currency" value="$">
</div>
<div class="col-md-2">
<label class="form-label">Period</label>
<input type="text" class="form-control plan-period" value="mo">
</div>
<div class="col-md-3">
<label class="form-label">Style</label>
<select class="form-select plan-style">
<option value="default" selected>Default</option>
<option value="style-2">Style 2 (Featured)</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Button Text</label>
<input type="text" class="form-control plan-button-text" value="Get Started Today">
</div>
<div class="col-md-4">
<label class="form-label">Button Link</label>
<input type="text" class="form-control plan-button-link" value="/pricing">
</div>
<div class="col-md-4">
<label class="form-label">Button Icon</label>
<input type="text" class="form-control plan-button-icon" value="fa-solid fa-arrow-right">
</div>
<div class="col-md-12">
<label class="form-label">Features (one per line)</label>
<textarea class="form-control plan-features" rows="4"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removePlan(this)">
<i class="fas fa-trash me-2"></i>Remove Plan
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removePlan(button) {
if (confirm('Are you sure you want to remove this plan?')) {
button.closest('.plan-item').remove();
}
}
function addTestimonial() {
const container = document.getElementById('testimonialsContainer');
const html = `
<div class="card mb-3 testimonial-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Name</label>
<input type="text" class="form-control testimonial-name" value="">
</div>
<div class="col-md-4">
<label class="form-label">Role/Type</label>
<input type="text" class="form-control testimonial-role" value="">
</div>
<div class="col-md-4">
<label class="form-label">Rating</label>
<select class="form-select testimonial-rating">
<option value="1">1 Star</option>
<option value="2">2 Stars</option>
<option value="3">3 Stars</option>
<option value="4">4 Stars</option>
<option value="5" selected>5 Stars</option>
</select>
</div>
<div class="col-md-12">
<label class="form-label">Content</label>
<textarea class="form-control testimonial-content" rows="3"></textarea>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeTestimonial(this)">
<i class="fas fa-trash me-2"></i>Remove Testimonial
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeTestimonial(button) {
if (confirm('Are you sure you want to remove this testimonial?')) {
button.closest('.testimonial-item').remove();
}
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
location.reload();
}
}
// Image uploader function
function openImageUploader(targetInput, imageType) {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
// Send imageType via query string as controller expects req.query.imageType
const uploadUrl = '/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general');
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.path) {
document.getElementById(targetInput).value = result.path;
if (targetInput === 'heroBackgroundImage') {
updateHeroImagePreview(result.path);
}
} else {
alert('Upload failed: ' + (result.error || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed. Please try again.');
}
};
input.click();
}
</script>

View File

@@ -0,0 +1,124 @@
<div class="page-title-area">
<div>
<h1>Create Qualification</h1>
<p class="subtitle">Register a new degree / qualification record</p>
</div>
<a href="/admin/qualification" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
<i class="fas fa-exclamation-circle"></i> <%= error %>
</div>
<% } %>
<form method="POST" action="/admin/qualification/create" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="qualification_number" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.qualification_number || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="student_name" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.student_name || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Program Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="program_name" required
value="<%= (typeof formData !== 'undefined' && formData) ? formData.program_name || '' : '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Passport No.</label>
<input type="text" class="form-control" name="passport_number"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.passport_number || '' : '' %>">
</div>
<div class="col-12">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.address || '' : '' %>">
</div>
</div>
</div>
</div>
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Department <span class="text-danger">*</span></label>
<select class="form-select" name="department" required>
<option value="">-- Select --</option>
<% departments.forEach(d => { %>
<option value="<%= d._id %>" <%= (typeof formData !== 'undefined' && formData && formData.department === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Level <span class="text-danger">*</span></label>
<select class="form-select" name="level" required>
<option value="">-- Select --</option>
<% levels.forEach(l => { %>
<option value="<%= l._id %>" <%= (typeof formData !== 'undefined' && formData && formData.level === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="issued_date" required
value="<%= (typeof formData !== 'undefined' && formData && formData.issued_date) ? formData.issued_date.toString().substring(0,10) : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="active">Active</option>
<option value="revoked" <%= (typeof formData !== 'undefined' && formData && formData.status === 'revoked') ? 'selected' : '' %>>Revoked</option>
</select>
</div>
</div>
</div>
</div>
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic <span style="font-size:0.75rem;color:var(--text-muted);font-weight:400">(fill for PhD — leave empty for MBA/Master)</span></h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Topic Title</label>
<input type="text" class="form-control" name="topic_name"
value="<%= (typeof formData !== 'undefined' && formData) ? formData.topic_name || '' : '' %>">
</div>
<div class="col-12">
<label class="form-label">Short Description</label>
<textarea class="form-control" name="topic_short_desc" rows="3"><%= (typeof formData !== 'undefined' && formData) ? formData.topic_short_desc || '' : '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Degree Image</h5></div>
<div class="card-body">
<input type="file" class="form-control" name="degree_image" accept="image/*">
</div>
</div>
<div class="card border-0">
<div class="card-body d-flex flex-column gap-2">
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Create Qualification</button>
<a href="/admin/qualification" class="btn btn-outline-secondary w-100">Cancel</a>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,123 @@
<div class="page-title-area">
<div>
<h1>Edit Qualification</h1>
<p class="subtitle"><code style="font-size:0.8rem;color:var(--accent-color)"><%= qual.qualification_number %></code></p>
</div>
<a href="/admin/qualification" class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> Back</a>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert d-flex align-items-center gap-2 mb-3" style="background:var(--danger-soft);color:var(--danger-color);border:none;border-radius:var(--border-radius-sm);">
<i class="fas fa-exclamation-circle"></i> <%= error %>
</div>
<% } %>
<form method="POST" action="/admin/qualification/<%= qual._id %>/edit" enctype="multipart/form-data">
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-id-card"></i> Basic Information</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Qualification No. <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="qualification_number" required value="<%= qual.qualification_number %>">
</div>
<div class="col-md-6">
<label class="form-label">Student Full Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="student_name" required value="<%= qual.student_name %>">
</div>
<div class="col-md-6">
<label class="form-label">Program Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="program_name" required value="<%= qual.program_name %>">
</div>
<div class="col-md-6">
<label class="form-label">Passport No.</label>
<input type="text" class="form-control" name="passport_number" value="<%= qual.passport_number || '' %>">
</div>
<div class="col-12">
<label class="form-label">Address</label>
<input type="text" class="form-control" name="address" value="<%= qual.address || '' %>">
</div>
</div>
</div>
</div>
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-tags"></i> Classification</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Department <span class="text-danger">*</span></label>
<select class="form-select" name="department" required>
<option value="">-- Select --</option>
<% departments.forEach(d => { %>
<option value="<%= d._id %>" <%= (qual.department && qual.department._id && qual.department._id.toString() === d._id.toString()) ? 'selected' : '' %>><%= d.name %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Level <span class="text-danger">*</span></label>
<select class="form-select" name="level" required>
<option value="">-- Select --</option>
<% levels.forEach(l => { %>
<option value="<%= l._id %>" <%= (qual.level && qual.level._id && qual.level._id.toString() === l._id.toString()) ? 'selected' : '' %>><%= l.type %></option>
<% }); %>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Issue Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" name="issued_date" required value="<%= qual.issued_date ? new Date(qual.issued_date).toISOString().substring(0,10) : '' %>">
</div>
<div class="col-md-4">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="active" <%= qual.status === 'active' ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= qual.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
</select>
</div>
</div>
</div>
</div>
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-book-open"></i> Thesis / Topic</h5></div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Topic Title</label>
<input type="text" class="form-control" name="topic_name" value="<%= qual.topic_name || '' %>">
</div>
<div class="col-12">
<label class="form-label">Short Description</label>
<textarea class="form-control" name="topic_short_desc" rows="3"><%= qual.topic_short_desc || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Degree Image</h5></div>
<div class="card-body">
<% if (qual.degree_image) { %>
<div class="mb-2">
<img src="/admin/files/<%= qual.degree_image %>" alt="Degree image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
</div>
<% } %>
<input type="file" class="form-control" name="degree_image" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
</div>
</div>
<div class="card border-0">
<div class="card-body d-flex flex-column gap-2">
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save"></i> Save Changes</button>
<a href="/admin/qualification" class="btn btn-outline-secondary w-100">Cancel</a>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,110 @@
<div class="page-title-area">
<div>
<h1>Qualifications</h1>
<p class="subtitle">Degree / qualification records</p>
</div>
<a href="/admin/qualification/create" class="btn btn-primary">
<i class="fas fa-plus"></i> New Qualification
</a>
</div>
<!-- Filter -->
<div class="card border-0 mb-3">
<div class="card-body" style="padding:1rem 1.25rem;">
<form method="GET" action="/admin/qualification">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<div class="position-relative search-bar">
<i class="fas fa-search search-icon"></i>
<input type="text" class="form-control" name="search" placeholder="Search name, number..." value="<%= query.search || '' %>">
</div>
</div>
<div class="col-md-2">
<select class="form-select" name="status">
<option value="">All Status</option>
<option value="active" <%= query.status === 'active' ? 'selected' : '' %>>Active</option>
<option value="revoked" <%= query.status === 'revoked' ? 'selected' : '' %>>Revoked</option>
</select>
</div>
<div class="col-md-1 d-flex gap-1">
<button type="submit" class="btn btn-primary flex-fill"><i class="fas fa-search"></i></button>
<% if (query.search || query.status) { %>
<a href="/admin/qualification" class="btn btn-outline-secondary"><i class="fas fa-times"></i></a>
<% } %>
</div>
</div>
</form>
</div>
</div>
<!-- Table -->
<div class="card border-0">
<div class="card-body p-0">
<% if (qualifications && qualifications.length > 0) { %>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th style="width:40px">#</th>
<th>Qualification No.</th>
<th>Full Name</th>
<th>Program</th>
<th>Department</th>
<th>Level</th>
<th>Issue Date</th>
<th>Type</th>
<th>Status</th>
<th style="width:90px">Actions</th>
</tr>
</thead>
<tbody>
<% qualifications.forEach((q, i) => { %>
<tr>
<td style="color:var(--text-muted)"><%= i + 1 %></td>
<td><code style="font-size:0.8rem;color:var(--primary-color)"><%= q.qualification_number %></code></td>
<td style="font-weight:500"><%= q.student_name %></td>
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= q.program_name %></td>
<td style="font-size:0.8125rem"><%= q.department ? q.department.name : '—' %></td>
<td style="font-size:0.8125rem"><%= q.level ? q.level.type : '—' %></td>
<td style="font-size:0.8125rem;color:var(--text-muted)"><%= q.issued_date ? new Date(q.issued_date).toLocaleDateString('en-GB') : '—' %></td>
<td>
<% if (q.topic_name) { %>
<span class="badge badge-soft-primary">PhD</span>
<% } else { %>
<span class="badge bg-soft-info">MBA/Master</span>
<% } %>
</td>
<td>
<% if (q.status === 'active') { %>
<span class="badge bg-soft-success">Active</span>
<% } else { %>
<span class="badge bg-soft-danger">Revoked</span>
<% } %>
</td>
<td>
<div class="table-actions">
<a href="/admin/qualification/<%= q._id %>/edit" class="btn btn-sm btn-outline-primary btn-icon" title="Edit">
<i class="fas fa-pen"></i>
</a>
<form method="POST" action="/admin/qualification/<%= q._id %>/delete" style="display:inline;" onsubmit="return confirm('Delete this qualification?')">
<button type="submit" class="btn btn-sm btn-outline-danger btn-icon" title="Delete">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
<% } else { %>
<div class="empty-state">
<div class="empty-state-icon"><i class="fas fa-graduation-cap"></i></div>
<h5>No qualifications found</h5>
<p>Create the first qualification record.</p>
<a href="/admin/qualification/create" class="btn btn-primary"><i class="fas fa-plus"></i> Create</a>
</div>
<% } %>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,755 +0,0 @@
<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);">Service Details: <%= service.name %></h1>
<p class="text-muted mb-0">Edit detailed content for <%= service.name %> service</p>
</div>
<div>
<a href="/admin/service" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Services
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/<%= service.slug %>/details/update" method="POST" class="content-with-fixed-buttons" id="serviceDetailsForm">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="details" id="detailsJson">
<input type="hidden" name="features" id="featuresJson">
<input type="hidden" name="faq" id="faqJson">
<!-- 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="#basic-info" role="tab">
<i class="fas fa-info-circle me-2"></i>Basic Information
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#key-features" role="tab">
<i class="fas fa-star me-2"></i>Key Features
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#faq-section" role="tab">
<i class="fas fa-question-circle me-2"></i>FAQ Section
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Basic Information Tab -->
<div class="tab-pane fade show active" id="basic-info" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-5">
<label class="form-label fw-medium">Main Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="mainImage" name="mainImage"
value="<%= service.details?.mainImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="mainImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="form-text text-muted">Recommended size: 800x600px</small>
</div>
<div class="col-md-7">
<div id="mainImagePreview">
<% if (service.details?.mainImage) { %>
<img src="<%= getFullImageUrl(service.details.mainImage) %>" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="detailsTitle" name="title"
value="<%= service.details?.title || service.name %>">
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="detailsDescription" name="description" rows="3"><%= service.details?.description || service.description %></textarea>
</div>
</div>
<!-- Overview Section -->
<div class="row mt-4">
<div class="col-12">
<div class="card border">
<div class="card-header bg-light">
<h6 class="mb-0">Overview Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Overview Title</label>
<input type="text" class="form-control" id="overviewTitle" name="overviewTitle"
value="<%= service.details?.overviewTitle || 'Service Overview' %>">
</div>
<div class="col-md-12">
<label class="form-label">Overview Description</label>
<textarea class="form-control" id="overviewDescription" name="overviewDescription" rows="4"><%= service.details?.overviewDescription || '' %></textarea>
</div>
<div class="col-md-12">
<label class="form-label">Additional Description</label>
<textarea class="form-control" id="additionalDescription" name="additionalDescription" rows="3"><%= service.details?.additionalDescription || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Key Features Tab -->
<div class="tab-pane fade" id="key-features" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- Features Header -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">Features Title</label>
<input type="text" class="form-control" id="keyFeaturesTitle" name="keyFeaturesTitle"
value="<%= service.details?.keyFeaturesTitle || 'Key Features' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Features Image</label>
<div class="input-group">
<input type="text" class="form-control" id="keyFeaturesImage" name="keyFeaturesImage"
value="<%= service.details?.keyFeaturesImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="keyFeaturesImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<!-- Key Features Image Preview -->
<div class="row mt-3">
<div class="col-12">
<div id="keyFeaturesImagePreview">
<% if (service.details?.keyFeaturesImage) { %>
<img src="<%= getFullImageUrl(service.details.keyFeaturesImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">
<% } %>
</div>
</div>
</div>
<!-- Features List -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Features</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFeature()">
<i class="fas fa-plus"></i> Add Feature
</button>
</div>
<div id="featuresContainer">
<% if (service.details?.features && service.details.features.length > 0) { %>
<% service.details.features.forEach((feature, index) => { %>
<div class="card mb-3 feature-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">Feature <%= index + 1 %></h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Title</label>
<input type="text" class="form-control feature-title"
value="<%= feature.title || '' %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control feature-description" rows="2" required><%= feature.description || '' %></textarea>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FAQ Section Tab -->
<div class="tab-pane fade" id="faq-section" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- FAQ Header -->
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-medium">FAQ Title</label>
<input type="text" class="form-control" id="faqTitle" name="faqTitle"
value="<%= service.details?.faqTitle || 'Frequently Asked Questions' %>">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">FAQ Image</label>
<div class="input-group">
<input type="text" class="form-control" id="faqImage" name="faqImage"
value="<%= service.details?.faqImage || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="faqImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<!-- FAQ Image Preview -->
<div class="row mt-3">
<div class="col-12">
<div id="faqImagePreview">
<% if (service.details?.faqImage) { %>
<img src="<%= getFullImageUrl(service.details.faqImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">
<% } %>
</div>
</div>
</div>
<!-- FAQ List -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">FAQ Items</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFAQ()">
<i class="fas fa-plus"></i> Add FAQ
</button>
</div>
<div id="faqContainer">
<% if (service.details?.faq && service.details.faq.length > 0) { %>
<% service.details.faq.forEach((faq, index) => { %>
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ <%= index + 1 %></h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID (Auto-generated)</label>
<input type="text" class="form-control faq-id"
value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
</div>
<div class="col-md-6">
<label class="form-label">Expanded by Default</label>
<select class="form-control faq-expanded">
<option value="false" <%= !faq.isExpanded ? 'selected' : '' %>>No</option>
<option value="true" <%= faq.isExpanded ? 'selected' : '' %>>Yes</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question"
value="<%= faq.question || '' %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer" rows="3" required><%= faq.answer || '' %></textarea>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<button type="reset" class="btn btn-secondary">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let originalFormData = null;
let featureIndex = <%= service.details?.features?.length || 0 %>;
let faqIndex = <%= service.details?.faq?.length || 0 %>;
document.addEventListener('DOMContentLoaded', function() {
// Initialize form data
originalFormData = <%- JSON.stringify(service) %>;
// Set initial JSON values
updateAllJsonInputs(originalFormData);
// Initialize form handlers
initializeFormHandlers();
});
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('serviceDetailsForm');
form.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
showError('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// 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);
});
});
// Initialize image input change listeners for manual URL input
const imageInputs = ['mainImage', 'keyFeaturesImage', 'faqImage'];
imageInputs.forEach(inputId => {
const input = document.getElementById(inputId);
if (input) {
input.addEventListener('input', function() {
updateImagePreviewAfterUpload(inputId, this.value);
});
}
});
}
function addFeature() {
const container = document.getElementById('featuresContainer');
const featureHtml = `
<div class="card mb-3 feature-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">Feature ${featureIndex + 1}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Title</label>
<input type="text" class="form-control feature-title" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Description</label>
<textarea class="form-control feature-description" rows="2" required></textarea>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', featureHtml);
featureIndex++;
}
function removeFeature(button) {
const featureItem = button.closest('.feature-item');
if (featureItem) {
featureItem.remove();
}
}
function addFAQ() {
const container = document.getElementById('faqContainer');
const newFaqId = generateFAQId();
const faqNumber = document.querySelectorAll('.faq-item').length + 1;
const faqHtml = `
<div class="card mb-3 faq-item">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0 text-decoration-underline">FAQ ${faqNumber}</h6>
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">ID (Auto-generated)</label>
<input type="text" class="form-control faq-id"
value="${newFaqId}" readonly>
</div>
<div class="col-md-6">
<label class="form-label">Expanded by Default</label>
<select class="form-control faq-expanded">
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Question</label>
<input type="text" class="form-control faq-question" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-12">
<label class="form-label">Answer</label>
<textarea class="form-control faq-answer" rows="3" required></textarea>
</div>
</div>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', faqHtml);
faqIndex++;
}
function updateFAQId(questionInput) {
// Không cần update ID nữa vì đã tự động theo số thứ tự
}
function removeFAQ(button) {
const faqItem = button.closest('.faq-item');
if (faqItem) {
faqItem.remove();
// Cập nhật lại số thứ tự và ID của tất cả FAQ
updateFAQNumbers();
}
}
function generateFAQId() {
// Đếm số lượng FAQ hiện tại và tạo ID tiếp theo
const existingFAQs = document.querySelectorAll('.faq-item');
const nextNumber = existingFAQs.length + 1;
return `faq-${nextNumber}`;
}
function updateFAQNumbers() {
// Cập nhật lại tất cả FAQ ID và số thứ tự
const faqItems = document.querySelectorAll('.faq-item');
faqItems.forEach((item, index) => {
const number = index + 1;
const idInput = item.querySelector('.faq-id');
const titleElement = item.querySelector('h6');
if (idInput) {
idInput.value = `faq-${number}`;
}
if (titleElement) {
titleElement.textContent = `FAQ ${number}`;
}
});
}
function updateAllJsonInputs(data) {
// Collect basic details data
const details = {
title: data.details?.title || data.name,
description: data.details?.description || data.description,
mainImage: data.details?.mainImage || '',
overviewTitle: data.details?.overviewTitle || 'Service Overview',
overviewDescription: data.details?.overviewDescription || '',
additionalDescription: data.details?.additionalDescription || '',
keyFeaturesTitle: data.details?.keyFeaturesTitle || 'Key Features',
keyFeaturesImage: data.details?.keyFeaturesImage || '',
faqTitle: data.details?.faqTitle || 'Frequently Asked Questions',
faqImage: data.details?.faqImage || ''
};
document.getElementById('detailsJson').value = JSON.stringify(details);
document.getElementById('featuresJson').value = JSON.stringify(data.details?.features || []);
document.getElementById('faqJson').value = JSON.stringify(data.details?.faq || []);
}
function updateJsonData() {
// Collect basic details data
const details = {
title: document.getElementById('detailsTitle').value,
description: document.getElementById('detailsDescription').value,
mainImage: document.getElementById('mainImage').value,
overviewTitle: document.getElementById('overviewTitle').value,
overviewDescription: document.getElementById('overviewDescription').value,
additionalDescription: document.getElementById('additionalDescription').value,
keyFeaturesTitle: document.getElementById('keyFeaturesTitle').value,
keyFeaturesImage: document.getElementById('keyFeaturesImage').value,
faqTitle: document.getElementById('faqTitle').value,
faqImage: document.getElementById('faqImage').value
};
// Collect features data
const features = [];
document.querySelectorAll('.feature-item').forEach(item => {
features.push({
title: item.querySelector('.feature-title').value,
description: item.querySelector('.feature-description').value
});
});
// Collect FAQ data
const faq = [];
document.querySelectorAll('.faq-item').forEach(item => {
faq.push({
id: item.querySelector('.faq-id').value,
question: item.querySelector('.faq-question').value,
answer: item.querySelector('.faq-answer').value,
isExpanded: item.querySelector('.faq-expanded').value === 'true'
});
});
document.getElementById('detailsJson').value = JSON.stringify(details);
document.getElementById('featuresJson').value = JSON.stringify(features);
document.getElementById('faqJson').value = JSON.stringify(faq);
}
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
// Nếu đã là full URL thì return luôn
if (imagePath.startsWith('http')) {
return imagePath;
}
// Lấy backend URL
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
// Xử lý đường dẫn
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
// Function để cập nhật image preview sau khi upload
function updateImagePreviewAfterUpload(targetInput, imagePath) {
const fullImageUrl = getFullImageUrlJS(imagePath);
switch(targetInput) {
case 'mainImage':
const mainPreview = document.getElementById('mainImagePreview');
if (mainPreview) {
mainPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">`;
}
break;
case 'keyFeaturesImage':
const featuresPreview = document.getElementById('keyFeaturesImagePreview');
if (featuresPreview) {
featuresPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">`;
}
break;
case 'faqImage':
const faqPreview = document.getElementById('faqImagePreview');
if (faqPreview) {
faqPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">`;
}
break;
}
}
async function openImageUploader(targetInput, imageType) {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return reject(new Error('No file selected'));
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(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');
}
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update image preview based on target input
updateImagePreviewAfterUpload(targetInput, result.path);
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
resolve(result);
} catch (err) {
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
console.error('Upload error:', err);
showError('Upload failed: ' + (err.message || 'Unknown error'));
reject(err);
} finally {
fileInput.remove();
}
};
fileInput.click();
});
}
function showSuccess(message) {
// Create and show success alert
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showError(message) {
// Create and show error alert
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
</script>
<style>
.bottom-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid #dee2e6;
}
.content-with-fixed-buttons {
/* Remove bottom padding since buttons are no longer fixed */
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
.card-header h6 {
color: var(--primary-dark);
}
.text-decoration-underline {
text-decoration: underline;
color: var(--primary-dark);
}
/* Image Preview Styles */
#mainImagePreview, #keyFeaturesImagePreview, #faqImagePreview {
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 10px;
transition: all 0.3s ease;
}
#mainImagePreview:empty::before,
#keyFeaturesImagePreview:empty::before,
#faqImagePreview:empty::before {
content: "No image selected";
color: #6c757d;
font-style: italic;
}
#mainImagePreview img,
#keyFeaturesImagePreview img,
#faqImagePreview img {
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>

View File

@@ -1,440 +0,0 @@
<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);">Edit Service: <%= service.name %></h1>
<p class="text-muted mb-0">Update service information and settings</p>
</div>
<div>
<a href="/admin/service" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Services
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/<%= service.slug %>/edit" method="POST" id="editServiceForm">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--primary-dark);">
<i class="fas fa-edit me-2"></i>Service Information
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label fw-medium">Service Name</label>
<input type="text" class="form-control" id="serviceName" name="name"
value="<%= service.name %>" required>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-6">
<label class="form-label fw-medium">
Slug
<small class="text-muted">(generated from name)</small>
<span id="slugAutoIndicator" class="badge bg-info ms-1" style="font-size: 0.7em;">EXISTING</span>
</label>
<div class="input-group">
<input type="text" class="form-control" id="serviceSlug" name="slug"
value="<%= service.slug %>" readonly>
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
<i class="fas fa-magic me-1"></i>Generate
</button>
</div>
<small class="form-text text-muted">URL-friendly version of the service name.</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Layout</label>
<select class="form-control" id="serviceLayout" name="layout">
<option value="left" <%= service.layout === 'left' ? 'selected' : '' %>>Left</option>
<option value="right" <%= service.layout === 'right' ? 'selected' : '' %>>Right</option>
</select>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-md-8">
<label class="form-label fw-medium">Image</label>
<div class="input-group">
<input type="text" class="form-control" id="serviceImage" name="image"
value="<%= service.image || '' %>">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="serviceImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-4">
<div id="serviceImagePreview">
<% if (service.image) { %>
<img src="<%= getFullImageUrl(service.image) %>" class="img-thumbnail"
style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">
<% } %>
</div>
</div>
</div>
<div class="row g-3 mt-2">
<div class="col-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" id="serviceDescription" name="description"
rows="3" required><%= service.description %></textarea>
</div>
</div>
</div>
</div>
<!-- Bottom Buttons -->
<div class="bottom-buttons">
<a href="/admin/service" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>Cancel
</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Update Service
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let servicesData = []; // Will be populated for duplicate checking
document.addEventListener('DOMContentLoaded', function() {
// Load existing services data for duplicate checking
loadServicesData();
// Initialize form handlers
initializeFormHandlers();
});
async function loadServicesData() {
try {
const response = await fetch('/api/service');
const data = await response.json();
servicesData = data.services?.items || [];
} catch (error) {
console.error('Error loading services data:', error);
servicesData = [];
}
}
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('editServiceForm');
form.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating...';
try {
// Check for duplicate slug before submitting
const slug = document.getElementById('serviceSlug').value.trim();
const currentSlug = '<%= service.slug %>';
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
showError('Service with this slug already exists. Please generate a new slug.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
return;
}
this.submit();
} catch (error) {
console.error('Error updating service:', error);
showError('Failed to update service. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
}
});
// 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);
});
});
// Image preview for service image
const serviceImageInput = document.getElementById('serviceImage');
if (serviceImageInput) {
serviceImageInput.addEventListener('input', function() {
updateImagePreview('serviceImagePreview', this.value);
});
}
// Generate slug from service name
const serviceNameInput = document.getElementById('serviceName');
const serviceSlugInput = document.getElementById('serviceSlug');
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
const generateSlugBtn = document.getElementById('generateSlugBtn');
if (serviceNameInput && serviceSlugInput && generateSlugBtn) {
// Generate slug button
generateSlugBtn.addEventListener('click', async function() {
const serviceName = serviceNameInput.value.trim();
if (serviceName) {
// Show loading state
const originalBtnHtml = this.innerHTML;
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
try {
const slug = await generateSlugFromText(serviceName);
const currentSlug = '<%= service.slug %>';
// Check for duplicate slug (excluding current service)
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
const uniqueSlug = generateUniqueSlug(slug);
serviceSlugInput.value = uniqueSlug;
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
} else {
serviceSlugInput.value = slug;
showSuccess('Slug generated successfully!');
}
if (slugAutoIndicator) {
slugAutoIndicator.textContent = 'GENERATED';
slugAutoIndicator.className = 'badge bg-success ms-1';
slugAutoIndicator.style.fontSize = '0.7em';
}
} catch (error) {
console.error('Error generating slug:', error);
showError('Failed to generate slug. Please try again.');
} finally {
// Restore button state
this.disabled = false;
this.innerHTML = originalBtnHtml;
}
} else {
showError('Please enter a service name first.');
}
});
}
}
// Generate slug using backend API
async function generateSlugFromText(text) {
try {
const response = await fetch('/admin/service/generate-slug', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: text })
});
const result = await response.json();
if (result.success) {
return result.slug;
} else {
throw new Error(result.message || 'Failed to generate slug');
}
} catch (error) {
console.error('Error generating slug:', error);
// Fallback to simple slug generation if API fails
return text
.toString()
.toLowerCase()
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '');
}
}
// Check if slug already exists
function isSlugDuplicate(slug, excludeIndex = -1) {
return servicesData.some((service, index) => {
return service && service.slug === slug && index !== excludeIndex;
});
}
// Generate unique slug by appending number
function generateUniqueSlug(baseSlug) {
let counter = 1;
let uniqueSlug = baseSlug;
while (isSlugDuplicate(uniqueSlug, -1)) {
uniqueSlug = baseSlug + '-' + counter;
counter++;
}
return uniqueSlug;
}
function updateImagePreview(previewId, imagePath) {
const preview = document.getElementById(previewId);
if (imagePath) {
const fullImageUrl = getFullImageUrlJS(imagePath);
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
} else {
preview.innerHTML = '';
}
}
// Helper function để tạo full URL cho ảnh
function getFullImageUrlJS(imagePath) {
if (!imagePath) return '';
if (imagePath.startsWith('http')) {
return imagePath;
}
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
let imgSrc = imagePath;
if (!imgSrc.startsWith('/')) {
imgSrc = '/uploads/' + imgSrc;
}
return backendUrl + imgSrc;
}
async function openImageUploader(targetInput, imageType) {
return new Promise((resolve, reject) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return reject(new Error('No file selected'));
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
if (uploadBtn) {
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
}
const response = await fetch(`/admin/upload/image?imageType=service`, {
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');
}
const input = document.getElementById(targetInput);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update preview
if (targetInput === 'serviceImage') {
updateImagePreview('serviceImagePreview', result.path);
}
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
resolve(result);
} catch (err) {
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
}
console.error('Upload error:', err);
showError('Upload failed: ' + (err.message || 'Unknown error'));
reject(err);
} finally {
fileInput.remove();
}
};
fileInput.click();
});
}
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showWarning(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
function showError(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
</script>
<style>
.bottom-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
padding: 15px 0;
border-top: 1px solid #dee2e6;
}
.card-header h5 {
color: var(--primary-dark);
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,353 +0,0 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
Travel Information Editor
</h1>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-primary preview-btn">
<i class="fas fa-eye me-2"></i>Preview
</button>
<button type="submit" form="travelForm" class="btn btn-primary" id="saveBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</div>
<form id="travelForm" action="/admin/travel/update" method="POST" class="needs-validation" novalidate>
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="page" id="pageJson">
<input type="hidden" name="content" id="contentJson">
<input type="hidden" name="enableScrollspy" id="enableScrollspyInput">
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Hero Section</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-5">
<div class="mb-3">
<label class="form-label">Background Image</label>
<div class="input-group">
<input type="text" class="form-control" id="heroBackgroundImage"
value="<%= data.hero?.backgroundImage || '' %>" readonly>
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="travel">
<i class="fas fa-upload"></i>
</button>
</div>
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
</div>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 200px; width: 100%;">
<% if (data.hero?.backgroundImage) { %>
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
style="height: 200px; width: 100%; object-fit: cover;" alt="Background image preview">
<% } else { %>
<div class="border rounded d-flex align-items-center justify-content-center h-100 bg-light">
<span class="text-muted">No image selected</span>
</div>
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label">Hero Title</label>
<textarea class="form-control" id="heroTitle" rows="2"><%= data.hero?.title || 'Travel Information' %></textarea>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Page Title</label>
<input type="text" class="form-control" id="pageTitle" value="<%= data.page?.title || 'Go and Grow Camp Travel Information' %>">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">As of / Year</label>
<input type="text" class="form-control" id="pageYear" value="<%= data.page?.year || '' %>">
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Editor</h5>
<p class="text-muted mb-0 small">Write content using the blog editor</p>
</div>
<div class="card-body">
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">SEO Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" class="form-control" id="metadataTitle" value="<%= data.page?.metadata?.title || '' %>">
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea class="form-control" id="metadataDescription" rows="3"><%= data.page?.metadata?.description || '' %></textarea>
</div>
</div>
</div>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Page Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enableScrollspy" <%= data.enableScrollspy ? 'checked' : '' %>>
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="card-title mb-0">Content Tips</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Terms & Conditions:</h6>
<ul class="mb-0 small">
<li>Use <strong>Header 2</strong> for main sections</li>
<li>Use <strong>Header 3</strong> for subsections</li>
<li>Use <strong>Lists</strong> for terms items</li>
<li>Use <strong>Conclusion</strong> tool for important notes</li>
<li>Use <strong>Quote</strong> for legal references</li>
</ul>
<hr class="my-2">
<h6><i class="fas fa-keyboard me-2"></i>Keyboard Shortcuts:</h6>
<ul class="mb-0 small">
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>: Convert list item to header</li>
<li><kbd>Tab</kbd> in list: Indent item</li>
<li><kbd>Backspace</kbd> at start: Exit list</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Travel Information Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<iframe id="previewFrame" style="width: 100%; height: 600px; border: none;"></iframe>
</div>
</div>
</div>
</div>
<input type="file" id="directImageUpload" style="display: none;" accept="image/*">
<input type="hidden" id="currentImageType">
<input type="hidden" id="currentTargetInput">
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
<script type="module">
import BlogEditor from '/js/editor.js';
// Logic xử lý lọc dữ liệu để tránh duplicate video và xóa paragraph rỗng
class TravelContentManager {
cleanEditorData(editorData) {
const cleanedBlocks = [];
const seenVideoIds = new Set();
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|shorts\/)?([A-Za-z0-9_-]{11})/;
(editorData.blocks || []).forEach(block => {
if (!block) return;
// 1. Xử lý Video Embed (Deduplication)
if (block.type === 'embed') {
const bd = block.data || {};
const source = bd.source || bd.embed || '';
const match = source.match(youtubeRegex);
const vid = bd.videoId || (match ? match[1] : null);
if (vid) {
if (seenVideoIds.has(vid)) return; // Bỏ qua nếu đã có video này
seenVideoIds.add(vid);
}
cleanedBlocks.push(block);
return;
}
// 2. Xử lý Paragraph (Xóa dòng trống hoặc dòng chỉ chứa link đã embed)
if (block.type === 'paragraph') {
const text = (block.data?.text || '').toString().trim();
if (text === '') return; // Xóa paragraph rỗng
const match = text.match(youtubeRegex);
if (match && match[1]) {
// Nếu paragraph chỉ chứa link YouTube, và ta sẽ có/đã có block embed cho nó, thì bỏ qua paragraph
return;
}
}
cleanedBlocks.push(block);
});
return { ...editorData, blocks: cleanedBlocks };
}
}
document.addEventListener('DOMContentLoaded', async () => {
let blogEditorInstance = null;
const travelData = <%- JSON.stringify(data) %>;
const initialContent = travelData?.content || { blocks: [] };
try {
blogEditorInstance = new BlogEditor('editorjs', initialContent, 'travel');
window.blogEditorInstance = blogEditorInstance;
} catch (error) {
console.error('Error initializing BlogEditor:', error);
}
const form = document.getElementById('travelForm');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
// Lấy dữ liệu thô
const rawData = await blogEditorInstance.save();
// Làm sạch dữ liệu trước khi đóng gói JSON
const travelManager = new TravelContentManager();
const cleanedData = travelManager.cleanEditorData(rawData);
const heroData = {
title: document.getElementById('heroTitle').value.trim(),
backgroundImage: document.getElementById('heroBackgroundImage').value.trim(),
};
const pageData = {
title: document.getElementById('pageTitle').value.trim(),
year: document.getElementById('pageYear')?.value.trim(),
metadata: {
title: document.getElementById('metadataTitle').value.trim(),
description: document.getElementById('metadataDescription').value.trim(),
},
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
document.getElementById('pageJson').value = JSON.stringify(pageData);
document.getElementById('contentJson').value = JSON.stringify(cleanedData);
document.getElementById('enableScrollspyInput').value = document.getElementById('enableScrollspy').checked;
form.submit();
} catch (error) {
console.error('Save error:', error);
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
// Preview
const previewBtn = document.querySelector('.preview-btn');
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
previewBtn.addEventListener('click', async function () {
try {
const editorData = await blogEditorInstance.save();
const travelManager = new TravelContentManager();
const cleanedData = travelManager.cleanEditorData(editorData);
const formData = new FormData();
formData.append('content', JSON.stringify(cleanedData));
formData.append('heroTitle', document.getElementById('heroTitle').value);
formData.append('heroBackgroundImage', document.getElementById('heroBackgroundImage').value);
formData.append('pageTitle', document.getElementById('pageTitle').value);
formData.append('pageYear', document.getElementById('pageYear')?.value || '');
const response = await fetch('/admin/travel/preview', { method: 'POST', body: formData });
const html = await response.text();
const previewFrame = document.getElementById('previewFrame');
const blob = new Blob([html], { type: 'text/html' });
previewFrame.src = URL.createObjectURL(blob);
previewModal.show();
} catch (error) {
console.error('Preview error:', error);
}
});
// Image Upload Helpers
document.querySelectorAll('.btn-upload-image').forEach(btn => {
btn.addEventListener('click', function () {
document.getElementById('currentImageType').value = this.getAttribute('data-image-type');
document.getElementById('currentTargetInput').value = this.getAttribute('data-target-input');
document.getElementById('directImageUpload').click();
});
});
document.getElementById('directImageUpload').addEventListener('change', async function () {
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 resp = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: 'POST', body: formData });
const result = await resp.json();
if (result.success) {
document.getElementById(targetInput).value = result.path;
if (targetInput === 'heroBackgroundImage') updateHeroImagePreview(result.path);
showToast('Uploaded successfully', 'success');
}
} catch (error) {
showToast('Upload failed', 'danger');
}
});
function updateHeroImagePreview(imageUrl) {
document.getElementById('heroImagePreview').innerHTML = `<img src="${imageUrl}" class="img-thumbnail" style="height: 200px; width: 100%; object-fit: cover;">`;
}
function showToast(message, type) {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
document.body.appendChild(toast);
new bootstrap.Toast(toast, { delay: 3000 }).show();
}
});
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
<%= title %> | CMS-SIMS
</title>
<!-- Bootstrap CSS -->
<title><%= title %> | ULDP Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Global CSS Variables -->
<link rel="stylesheet" href="/assets/css/variables.css">
<!-- Custom CSS -->
<style>
body {
font-family: var(--font-family);
@@ -21,203 +14,257 @@
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 55%, var(--primary-light) 100%);
position: relative;
background-color: var(--primary-color);
overflow: hidden;
}
.main-content {
body::before {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.login-wrapper {
width: 100%;
max-width: 500px;
padding: 20px;
max-width: 420px;
padding: 1.25rem;
position: relative;
z-index: 1;
}
.login-container {
background-color: var(--bg-card);
padding: 30px;
.login-card {
background: #fff;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
}
.logo-container {
text-align: center;
margin-bottom: 10px;
}
.logo-container img {
width: 100px;
height: auto;
margin-bottom: 5px;
}
.img-shine {
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
overflow: hidden;
}
.img-shine::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s ease;
.login-header {
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
padding: 2rem 2rem 1.5rem;
text-align: center;
}
.img-shine:hover::after {
left: 100%;
.login-logo {
width: 150px;
height: 100%;
border-radius: 14px;
/* background: rgba(255,255,255,0.15); */
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
backdrop-filter: blur(4px);
}
.login-logo img {
width: 40px;
height: 40px;
object-fit: contain;
filter: brightness(0) invert(1);
}
.login-title {
color: #fff;
font-size: 1.2rem;
font-weight: 700;
margin: 0 0 0.25rem;
}
.login-subtitle {
color: rgba(255,255,255,0.65);
font-size: 0.8125rem;
margin: 0;
}
.login-body { padding: 1.75rem 2rem 2rem; }
.form-label {
font-weight: 600;
font-size: 0.8125rem;
color: var(--text-main);
margin-bottom: 0.35rem;
}
.form-control {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 0.6rem 0.875rem;
font-size: 0.9rem;
transition: var(--transition-base);
height: 42px;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(10,35,71,0.08);
outline: none;
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.password-wrapper { position: relative; }
.btn-shine {
background: var(--primary-color);
color: white;
.password-toggle {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
border: none;
padding: 12px 30px;
border-radius: 8px;
font-weight: 600;
font-size: 15px;
background: none;
color: var(--text-muted);
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
padding: 0;
font-size: 0.9rem;
z-index: 5;
}
.password-toggle:hover { color: var(--primary-color); }
.btn-login {
background: var(--primary-color);
color: #fff;
border: none;
width: 100%;
height: 44px;
border-radius: var(--border-radius-sm);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: var(--transition-base);
letter-spacing: 0.02em;
position: relative;
overflow: hidden;
width: 100%;
min-width: 150px;
}
.btn-shine:hover {
.btn-login:hover {
background: var(--primary-light);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(10, 35, 71, 0.2);
box-shadow: 0 6px 20px rgba(10,35,71,0.3);
}
.btn-shine::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s ease;
.accent-link {
color: var(--accent-color);
font-weight: 600;
text-decoration: none;
font-size: 0.8125rem;
}
.btn-shine:hover::after {
left: 100%;
.accent-link:hover { color: var(--accent-light); }
.login-footer {
text-align: center;
margin-top: 1.25rem;
}
@media (max-width: 768px) {
.main-content {
padding: 15px;
}
.login-footer p {
font-size: 0.75rem;
color: rgba(255,255,255,0.45);
margin: 0;
}
.login-container {
padding: 20px;
}
.toast-area {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 9999;
}
</style>
</head>
<body>
<div class="main-content">
<!-- Flash Messages Data (Hidden) -->
<% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %>
<div id="flash-messages-data" style="display: none;"><%- JSON.stringify({ success_msg: typeof success_msg
!=='undefined' && success_msg ? success_msg : null, error_msg: typeof error_msg !=='undefined' && error_msg ?
error_msg : null, error: typeof error !=='undefined' && error ? error : null }) %></div>
<% } %>
<% if(typeof success_msg !== 'undefined' || typeof error_msg !== 'undefined' || typeof error !== 'undefined') { %>
<div id="flash-messages-data" style="display:none;"><%- JSON.stringify({
success_msg: typeof success_msg !== 'undefined' && success_msg ? success_msg : null,
error_msg: typeof error_msg !== 'undefined' && error_msg ? error_msg : null,
error: typeof error !== 'undefined' && error ? error : null
}) %></div>
<% } %>
<div class="login-container">
<div class="logo-container img-shine">
<img src="/img/logo/logo-hai-learning.png" alt="Logo">
<div class="login-wrapper">
<div class="login-card">
<div class="login-header">
<div class="login-logo">
<img src="/img/logo.png" alt="ULDP Logo" style="width:100%;height:100%;object-fit:contain;filter:none;border-radius:0;">
</div>
<p class="login-subtitle">Sign in to your account</p>
</div>
<div class="login-body">
<form action="/auth/login" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required autocomplete="username" autofocus>
</div>
<div style="text-align: center; margin-bottom: 20px;">
<h4 style="color: var(--primary-color); font-weight: 600; margin-bottom: 5px;">CMS Management System</h4>
<p style="color: var(--text-color); font-size: 13px;">Welcome to Content Management System</p>
</div>
<form action="/auth/login" method="POST" class="login-form">
<div class="form-group" style="margin-bottom: 12px;">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required autocomplete="username"
autofocus>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="password" class="form-label mb-0">Password</label>
<a href="/auth/change-password" class="accent-link">Change Password?</a>
</div>
<div class="form-group" style="margin-bottom: 12px;">
<label for="password" class="form-label">Password</label>
<div style="position: relative;">
<input type="password" class="form-control" id="password" name="password" required
autocomplete="current-password" style="padding-right: 40px;">
<button type="button" class="password-toggle-btn" aria-label="Show password"
style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%); border: none; background: none; color: #666; cursor: pointer; padding: 0; z-index: 10;">
<i class="fas fa-eye"></i>
</button>
</div>
<a href="/auth/change-password"
style="display: block; text-align: right; margin-top: 8px; color: #2563eb; text-decoration: none; font-size: 13px; font-weight: 600;">
Change Password?
</a>
</div>
<div style="text-align: center; margin-top: 20px;">
<button type="submit" class="btn-shine">
Login
<div class="password-wrapper">
<input type="password" class="form-control" id="password" name="password" required autocomplete="current-password" style="padding-right:2.5rem">
<button type="button" class="password-toggle" id="togglePassword" aria-label="Toggle password">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div style="text-align: center; margin-top: 20px;">
<span style="font-size: 14px; color: #666;">Don't have an account?</span>
<a href="/auth/register"
style="color: var(--primary-color); font-weight: 600; text-decoration: none; margin-left: 5px;">
Create Account
</a>
</div>
</form>
</div>
<div class="mt-4">
<button type="submit" class="btn-login">
<i class="fas fa-sign-in-alt me-2"></i>Sign In
</button>
</div>
<div style="margin-top: 20px; text-align: center;">
<p style="font-size: 12px; color: #fff;">© 2024 Swiss Institute of Management and Sciences. All rights
reserved.</p>
</div>
<div class="text-center mt-3">
<span style="font-size:0.8125rem;color:var(--text-muted)">Don't have an account?</span>
<a href="/auth/register" class="accent-link ms-1">Create Account</a>
</div>
</form>
</div>
</div>
<div class="login-footer">
<p>&copy; <%= new Date().getFullYear() %> ULDP Management System. All rights reserved.</p>
</div>
</div>
<!-- Bootstrap JS -->
<div class="toast-area" id="toastArea"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Password toggle
document.getElementById('togglePassword').addEventListener('click', function() {
const pwd = document.getElementById('password');
const icon = this.querySelector('i');
if (pwd.type === 'password') {
pwd.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
pwd.type = 'password';
icon.className = 'fas fa-eye';
}
});
<!-- Toast JS -->
<script src="/js/toast.js"></script>
<!-- Flash Handler JS -->
<script src="/js/flash-handler.js"></script>
<!-- Password Toggle JS -->
<script src="/js/password-toggle.js"></script>
// Flash messages
const flashData = document.getElementById('flash-messages-data');
if (flashData) {
try {
const msgs = JSON.parse(flashData.textContent);
const area = document.getElementById('toastArea');
const show = (msg, type) => {
if (!msg) return;
const t = document.createElement('div');
t.className = `toast align-items-center text-bg-${type} border-0 show mb-2`;
t.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" onclick="this.closest('.toast').remove()"></button></div>`;
area.appendChild(t);
setTimeout(() => t.remove(), 4500);
};
show(msgs.success_msg, 'success');
show(msgs.error_msg, 'danger');
show(msgs.error, 'danger');
} catch(e) {}
}
</script>
</body>
</html>
</html>

View File

@@ -1,191 +0,0 @@
<!-- Hero Section -->
<section class="container">
<div class="hero"
style="background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); color: white; border-radius: 20px; box-shadow: 0 15px 30px rgba(0,0,0,0.1); margin-top: 2rem; overflow: hidden; position: relative;">
<div class="position-absolute top-0 end-0 p-3 d-none d-lg-block">
<div class="d-flex gap-2">
<div class="rounded-circle bg-white bg-opacity-10" style="width: 12px; height: 12px;"></div>
<div class="rounded-circle bg-white bg-opacity-10" style="width: 12px; height: 12px;"></div>
<div class="rounded-circle bg-white bg-opacity-10" style="width: 12px; height: 12px;"></div>
</div>
</div>
<div class="row align-items-center">
<div class="col-lg-6 p-5">
<h1 class="fw-bold mb-4 text-white">API Management</h1>
<p class="lead mb-4 text-white-50">Simple dashboard to control your APIs</p>
<div class="d-flex gap-3">
<% if (locals.user) { %>
<a href="/admin/dashboard" class="btn btn-lg"
style="background-color: white; color: var(--primary-color); font-weight: 600;">
<i class="fas fa-tachometer-alt me-2"></i>Go to Dashboard
</a>
<% } else { %>
<a href="/auth/login" class="btn btn-lg"
style="background-color: white; color: var(--primary-color); font-weight: 600;">
<i class="fas fa-sign-in-alt me-2"></i>Login
</a>
<% } %>
</div>
</div>
<div class="col-lg-6 d-none d-lg-block text-center">
<div style="position: relative; z-index: 1;">
<div class="position-absolute"
style="top: -20px; right: -20px; width: 100px; height: 100px; background: rgba(255,255,255,0.1); border-radius: 50%; z-index: -1;">
</div>
<div class="position-absolute"
style="bottom: -30px; left: -20px; width: 150px; height: 150px; background: rgba(255,255,255,0.1); border-radius: 50%; z-index: -1;">
</div>
</div>
</div>
</div>
</div>
</section>
<% if (locals.user) { %>
<!-- Features Section -->
<section class="container py-5">
<div class="card border-0 shadow-sm" style="border-radius: 20px; overflow: hidden;">
<div class="card-body p-4 p-lg-5">
<div class="text-center mb-5">
<span class="badge rounded-pill px-3 py-2 mb-2"
style="background-color: rgba(184, 183, 106, 0.1); color: var(--primary-color);">Features</span>
<h2 class="fw-bold" style="color: var(--primary-color);">Key Features</h2>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius: 15px; transition: transform 0.3s;">
<div class="card-body p-4">
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-4"
style="width: 60px; height: 60px;">
<i class="fas fa-lock fa-lg" style="color: var(--primary-color);"></i>
</div>
<h4 class="card-title fw-bold" style="color: var(--primary-color);">High Security</h4>
<p class="card-text text-muted">Secure user authentication system with password encryption.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius: 15px; transition: transform 0.3s;">
<div class="card-body p-4">
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-4"
style="width: 60px; height: 60px;">
<i class="fas fa-chart-line fa-lg" style="color: var(--primary-color);"></i>
</div>
<h4 class="card-title fw-bold" style="color: var(--primary-color);">Analytics Dashboard</h4>
<p class="card-text text-muted">Monitor and analyze data through visual charts.</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius: 15px; transition: transform 0.3s;">
<div class="card-body p-4">
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-4"
style="width: 60px; height: 60px;">
<i class="fas fa-cogs fa-lg" style="color: var(--primary-color);"></i>
</div>
<h4 class="card-title fw-bold" style="color: var(--primary-color);">API Integration</h4>
<p class="card-text text-muted">Provides API endpoints for easy data integration into other
applications.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="container py-5">
<div class="card border-0 shadow-sm"
style="border-radius: 20px; overflow: hidden; background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); color: white;">
<div class="card-body p-4 p-lg-5">
<div class="row align-items-center">
<div class="col-lg-8 mx-auto text-center">
<h2 class="fw-bold mb-4 text-white">Start Using CMS.HAILearning Today</h2>
<p class="lead mb-4 text-white-50">Experience simple and effective API management system</p>
<a href="/admin/dashboard" class="btn btn-lg"
style="background-color: white; color: var(--primary-color); font-weight: 600; border-radius: 10px; padding: 12px 30px;">
<i class="fas fa-tachometer-alt me-2"></i>Go to Dashboard
</a>
</div>
</div>
</div>
</div>
</section>
<% } else { %>
<!-- Info Section for non-logged in users -->
<section class="container py-5">
<div class="row g-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100" style="border-radius: 20px; overflow: hidden;">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-shield-alt fa-lg" style="color: var(--primary-color);"></i>
</div>
<h3 class="fw-bold mb-0" style="color: var(--primary-color);">Secure Access</h3>
</div>
<p class="text-muted mb-4">Our CMS provides secure access to manage your content with advanced
authentication and authorization features.</p>
<ul class="list-unstyled mb-0">
<li class="d-flex align-items-center mb-3">
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
<span class="text-muted">Role-based access control</span>
</li>
<li class="d-flex align-items-center mb-3">
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
<span class="text-muted">Encrypted data transmission</span>
</li>
<li class="d-flex align-items-center">
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
<span class="text-muted">Secure session management</span>
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100" style="border-radius: 20px; overflow: hidden;">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
<i class="fas fa-code fa-lg" style="color: var(--primary-color);"></i>
</div>
<h3 class="fw-bold mb-0" style="color: var(--primary-color);">API Integration</h3>
</div>
<p class="text-muted mb-4">Access our comprehensive API documentation and integrate your applications
seamlessly.</p>
<ul class="list-unstyled mb-4">
<li class="d-flex align-items-center mb-3">
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
<span class="text-muted">RESTful API endpoints</span>
</li>
<li class="d-flex align-items-center mb-3">
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
<span class="text-muted">JSON data format</span>
</li>
<li class="d-flex align-items-center">
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
<span class="text-muted">Detailed documentation</span>
</li>
</ul>
<div class="text-center mt-auto">
<a href="/auth/login" class="btn btn-primary px-4 py-2" style="border-radius: 10px;">
<i class="fas fa-sign-in-alt me-2"></i>Login to Access
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<% } %>

View File

@@ -1,156 +1,403 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" type="image/png" href="/uploads/layout/favicon.png" />
<link rel="icon" type="image/png" href="/img/favicon.png" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
<%= typeof title !=='undefined' ? title + ' | ' : '' %>CMS.HAILearning
</title>
<!-- Bootstrap CSS -->
<title><%= typeof title !== 'undefined' ? title + ' | ' : '' %>ULDP Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<link rel="stylesheet" href="/assets/css/variables.css" />
<link rel="stylesheet" href="/assets/css/components/button.css" />
<link rel="stylesheet" href="/assets/css/components/card.css" />
<link rel="stylesheet" href="/assets/css/components/form.css" />
<link rel="stylesheet" href="/assets/css/components/modal.css" />
<link rel="stylesheet" href="/assets/css/components/table.css" />
<link rel="stylesheet" href="/assets/css/layout.css" />
<style>
:root {
--primary-color: #b8b76a;
--primary-light: #c9c88a;
--primary-dark: #9a994a;
--sidebar-width: 248px;
--topbar-height: 58px;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: white;
body {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--bg-body);
font-family: var(--font-family);
}
.btn-primary:hover {
background-color: var(--primary-dark);
border-color: var(--primary-dark);
/* ── Topbar ── */
.admin-topbar {
position: fixed;
top: 0; left: 0; right: 0;
height: var(--topbar-height);
background-color: #fff;
box-shadow: var(--shadow-header);
display: flex;
align-items: center;
padding: 0 1.25rem 0 0;
z-index: 1030;
}
.nav-link:hover {
color: var(--primary-color);
.topbar-brand {
width: var(--sidebar-width);
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.2rem;
padding: 0.4rem 1rem;
text-decoration: none;
background: var(--primary-color);
height: 100%;
}
.nav-link.active {
color: var(--primary-color);
font-weight: 600;
.topbar-brand img {
width: 100px;
height: 80%;
object-fit: contain;
border-radius: 0;
margin-top: 20px;
}
.topbar-brand-text {
font-weight: var(--font-weight-semibold);
font-size: 0.7rem;
color: rgba(255,255,255,0.85);
line-height: 1;
letter-spacing: 0.04em;
text-transform: uppercase;
text-align: center;
}
.topbar-brand-sub {
display: none;
}
.topbar-body {
flex: 1;
display: flex;
align-items: center;
padding: 0 1.25rem;
gap: 0.75rem;
}
.topbar-page-title {
font-size: 0.9rem;
font-weight: var(--font-weight-semibold);
color: var(--text-main);
}
.topbar-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.5rem;
}
.topbar-divider {
width: 1px;
height: 22px;
background: var(--border-color);
}
.topbar-user-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
border-radius: var(--border-radius-sm);
border: 1px solid var(--border-color);
background: #fff;
cursor: pointer;
transition: var(--transition-base);
text-decoration: none;
}
.topbar-user-btn:hover { background: #f8fafc; border-color: #cbd5e1; }
.topbar-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: var(--font-weight-bold);
flex-shrink: 0;
}
.topbar-username {
font-size: 0.8125rem;
font-weight: var(--font-weight-medium);
color: var(--text-main);
}
/* ── Sidebar ── */
.admin-sidebar {
position: fixed;
top: var(--topbar-height);
left: 0;
bottom: 0;
width: var(--sidebar-width);
background: var(--sidebar-bg);
overflow-y: auto;
overflow-x: hidden;
z-index: 1020;
display: flex;
flex-direction: column;
}
.admin-sidebar::-webkit-scrollbar { width: 4px; }
.admin-sidebar::-webkit-scrollbar-track { background: transparent; }
.admin-sidebar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 4px; }
.sidebar-nav-area { padding: 0.75rem 0; flex: 1; }
.sidebar-section-label {
padding: 0.6rem 1.25rem 0.3rem;
font-size: 0.65rem;
font-weight: var(--font-weight-bold);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--sidebar-section-color);
margin-top: 0.25rem;
}
.admin-sidebar .nav-link {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.6rem 1.25rem;
color: var(--sidebar-text);
font-size: 0.875rem;
font-weight: var(--font-weight-medium);
border-radius: 0;
transition: var(--transition-base);
border-left: 3px solid transparent;
text-decoration: none;
position: relative;
}
.admin-sidebar .nav-link .nav-icon {
width: 18px;
text-align: center;
font-size: 0.85rem;
opacity: 0.7;
transition: var(--transition-base);
flex-shrink: 0;
}
.admin-sidebar .nav-link:hover {
background-color: var(--sidebar-hover-bg);
color: #fff;
border-left-color: rgba(188,159,105,0.5);
}
.admin-sidebar .nav-link:hover .nav-icon { opacity: 1; }
.admin-sidebar .nav-link.active {
background-color: var(--sidebar-active-bg);
color: #fff !important;
border-left-color: var(--sidebar-active-border);
font-weight: var(--font-weight-semibold);
}
.admin-sidebar .nav-link.active .nav-icon {
opacity: 1;
color: var(--accent-color);
}
.sidebar-divider {
border-top: 1px solid rgba(255,255,255,0.07);
margin: 0.5rem 1.25rem;
}
.sidebar-footer {
padding: 1rem 1.25rem;
border-top: 1px solid rgba(255,255,255,0.07);
}
.sidebar-footer-text {
font-size: 0.65rem;
color: rgba(255,255,255,0.3);
text-align: center;
}
/* ── Main content ── */
.admin-main {
margin-left: var(--sidebar-width);
margin-top: var(--topbar-height);
min-height: calc(100vh - var(--topbar-height));
padding: 1.5rem 1.75rem;
}
/* ── Flash toast ── */
#flash-messages-data { display: none; }
.toast-container {
position: fixed;
top: calc(var(--topbar-height) + 1rem);
right: 1.25rem;
z-index: 9999;
}
/* ── Responsive ── */
@media (max-width: 767.98px) {
.admin-sidebar { transform: translateX(-100%); }
.admin-main { margin-left: 0; padding: 1rem; }
.topbar-brand { width: auto; }
.topbar-page-title { display: none; }
}
</style>
<%- style %>
</head>
<body>
<div class="container-fluid">
<div class="row">
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/admin/dashboard">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/header">Header & Menu</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/footer">Footer</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/about-us">About Us</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/contact">Contact</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/appointment">Appointment</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/faq">FAQ</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/terms-conditions">Terms & Conditions</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/safety">Safety</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/blog">Blog</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/visa">Visa</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/blog-category">Blog Category</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/blog-tag">Blog Tag</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/form">Form</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/upload">Upload</a>
</li>
</ul>
</div>
<!-- Topbar -->
<header class="admin-topbar">
<a href="/admin/dashboard" class="topbar-brand">
<img src="/img/logo.png" alt="ULDP Logo" />
<div class="topbar-brand-text">ULDP Management</div>
</a>
<div class="topbar-body">
<span class="topbar-page-title"><%= typeof title !== 'undefined' ? title : 'Dashboard' %></span>
<div class="topbar-right">
<% if (locals.user) { %>
<a class="topbar-user-btn" href="#">
<div class="topbar-avatar">
<%= (locals.user.username || locals.user.email || 'A')[0].toUpperCase() %>
</div>
<span class="topbar-username d-none d-md-inline">
<%= locals.user.username || locals.user.email %>
</span>
</a>
<div class="topbar-divider"></div>
<a href="/auth/logout" class="btn btn-sm btn-outline-danger">
<i class="fas fa-sign-out-alt"></i>
<span class="d-none d-md-inline">Logout</span>
</a>
<% } %>
</div>
</div>
</header>
<!-- Sidebar -->
<aside class="admin-sidebar">
<div class="sidebar-nav-area">
<div class="sidebar-section-label">Main</div>
<nav>
<a class="nav-link <%= currentPath === '/admin/dashboard' ? 'active' : '' %>" href="/admin/dashboard">
<i class="fas fa-chart-pie nav-icon"></i>
<span>Dashboard</span>
</a>
<a class="nav-link <%= currentPath && currentPath.startsWith('/admin/qualification') ? 'active' : '' %>" href="/admin/qualification">
<i class="fas fa-graduation-cap nav-icon"></i>
<span>Qualifications</span>
</a>
<a class="nav-link <%= currentPath && currentPath.startsWith('/admin/certificate') ? 'active' : '' %>" href="/admin/certificate">
<i class="fas fa-certificate nav-icon"></i>
<span>Certificates</span>
</a>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<!-- Flash Messages Data (Hidden) -->
<% if(typeof success_msg !=='undefined' || typeof error_msg !=='undefined' || typeof error !=='undefined' ) { %>
<div id="flash-messages-data" style="display: none">
<%- JSON.stringify({ success_msg: typeof success_msg !=='undefined' && success_msg ? success_msg : null,
error_msg: typeof error_msg !=='undefined' && error_msg ? error_msg : null, error: typeof error
!=='undefined' && error ? error : null }) %>
</div>
<% } %>
<div class="sidebar-divider"></div>
<div class="sidebar-section-label">Configuration</div>
<nav>
<a class="nav-link <%= currentPath === '/admin/department' ? 'active' : '' %>" href="/admin/department">
<i class="fas fa-building nav-icon"></i>
<span>Departments</span>
</a>
<a class="nav-link <%= currentPath === '/admin/level' ? 'active' : '' %>" href="/admin/level">
<i class="fas fa-layer-group nav-icon"></i>
<span>Levels</span>
</a>
</nav>
<div class="sidebar-divider"></div>
<div class="sidebar-section-label">System</div>
<nav>
<a class="nav-link <%= currentPath && currentPath.startsWith('/admin/audit-logs') ? 'active' : '' %>" href="/admin/audit-logs">
<i class="fas fa-shield-alt nav-icon"></i>
<span>Audit Logs</span>
</a>
</nav>
<div class="py-3"><%- body %></div>
</main>
</div>
</div>
<div class="sidebar-footer">
<div class="sidebar-footer-text">ULDP &copy; <%= new Date().getFullYear() %></div>
</div>
</aside>
<!-- Main Content -->
<main class="admin-main">
<% if(typeof success_msg !== 'undefined' || typeof error_msg !== 'undefined' || typeof error !== 'undefined') { %>
<div id="flash-messages-data">
<%- JSON.stringify({
success_msg: typeof success_msg !== 'undefined' && success_msg ? success_msg : null,
error_msg: typeof error_msg !== 'undefined' && error_msg ? error_msg : null,
error: typeof error !== 'undefined' && error ? error : null
}) %>
</div>
<% } %>
<%- body %>
</main>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Global function to clean up any stuck modal backdrops
// Modal cleanup
function forceCleanupModals() {
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
const backdrops = document.querySelectorAll('.modal-backdrop, .overlay, .loading');
if (backdrops.length > 0) {
console.warn('Force removing stuck backdrops:', backdrops.length);
backdrops.forEach(el => el.remove());
}
document.querySelectorAll('.modal-backdrop, .overlay, .loading').forEach(el => el.remove());
}
// Automatically clean up on every hide event
document.addEventListener('hidden.bs.modal', function () {
// Wait a tiny bit for the animation to finish
setTimeout(forceCleanupModals, 100);
});
// Watchdog: Check if backdrops exist without a visible modal every 2 seconds
document.addEventListener('hidden.bs.modal', () => setTimeout(forceCleanupModals, 100));
setInterval(() => {
const visibleModals = document.querySelectorAll('.modal.show');
if (visibleModals.length === 0) {
const backdrops = document.querySelectorAll('.modal-backdrop');
if (backdrops.length > 0) {
forceCleanupModals();
}
}
if (!document.querySelector('.modal.show') && document.querySelector('.modal-backdrop')) forceCleanupModals();
}, 2000);
// Clean up on page load
window.addEventListener('load', forceCleanupModals);
// Flash toast
const flashData = document.getElementById('flash-messages-data');
if (flashData) {
try {
const msgs = JSON.parse(flashData.textContent);
const container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
const show = (msg, type) => {
if (!msg) return;
const icons = { success: 'fa-check-circle', danger: 'fa-exclamation-circle', warning: 'fa-exclamation-triangle' };
const t = document.createElement('div');
t.className = `toast align-items-center text-bg-${type} border-0 show mb-2`;
t.setAttribute('role', 'alert');
t.innerHTML = `<div class="d-flex"><div class="toast-body d-flex align-items-center gap-2"><i class="fas ${icons[type] || 'fa-info-circle'}"></i>${msg}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
container.appendChild(t);
setTimeout(() => t.remove(), 4500);
};
show(msgs.success_msg, 'success');
show(msgs.error_msg, 'danger');
show(msgs.error, 'danger');
} catch(e) {}
}
</script>
<%- script %>
</body>
</html>
</html>

View File

@@ -1,83 +1,103 @@
<div class="error-page">
<div class="error-container">
<h1 class="error-title">404</h1>
<h2 class="error-subtitle">Oops! Page Not Found</h2>
<p class="error-text">Sorry, the page you are looking for does not exist or has been moved.</p>
<div class="error-actions">
<a href="/" class="btn btn-primary">Back to Home</a>
<a href="/contact" class="btn btn-outline-primary">Contact Support</a>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 — Page Not Found | ULDP Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="/assets/css/variables.css">
<style>
body {
font-family: var(--font-family, "Inter", sans-serif);
background: var(--bg-body, #f0f2f7);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.error-wrap {
text-align: center;
padding: 2rem 1.5rem;
max-width: 480px;
}
.error-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 120px;
height: 120px;
border-radius: 50%;
background: var(--primary-soft, rgba(10,35,71,0.07));
margin-bottom: 1.5rem;
}
.error-badge i {
font-size: 3rem;
color: var(--primary-color, #0a2347);
opacity: 0.6;
}
.error-code {
font-size: 5rem;
font-weight: 800;
color: var(--primary-color, #0a2347);
line-height: 1;
letter-spacing: -0.03em;
margin-bottom: 0.5rem;
}
.error-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 0.5rem;
}
.error-desc {
color: #64748b;
font-size: 0.9rem;
margin-bottom: 2rem;
line-height: 1.6;
}
.btn-home {
background: var(--primary-color, #0a2347);
color: #fff;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 6px;
font-weight: 500;
font-size: 0.9rem;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
}
.btn-home:hover {
background: #1a3a6b;
color: #fff;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(10,35,71,0.25);
}
</style>
</head>
<body>
<div class="error-wrap">
<div class="error-badge">
<i class="fas fa-map-signs"></i>
</div>
<div class="error-code">404</div>
<h1 class="error-title">Page Not Found</h1>
<p class="error-desc">The page you're looking for doesn't exist or has been moved.</p>
<a href="/admin/dashboard" class="btn-home">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<style>
.error-page {
min-height: calc(100vh - 200px); /* Trừ đi header và footer */
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.error-container {
text-align: center;
max-width: 600px;
padding: 40px 20px;
}
.error-title {
font-size: 120px;
color: linear-gradient(90deg, #067f3f, #056633, #044e27);
background: -webkit-linear-gradient(90deg, #067f3f, #056633, #044e27);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
line-height: 1;
font-weight: 700;
}
.error-subtitle {
font-size: 28px;
color: #333;
margin: 20px 0;
}
.error-text {
font-size: 18px;
color: #666;
margin-bottom: 30px;
}
.error-actions {
display: flex;
justify-content: center;
gap: 20px;
}
.btn-outline-primary:hover {
border-color: white;
}
@media (max-width: 768px) {
.error-title {
font-size: 80px;
}
.error-subtitle {
font-size: 24px;
}
.error-text {
font-size: 16px;
}
.error-actions {
flex-direction: column;
gap: 10px;
}
.error-actions .btn {
width: 100%;
}
}
</style>
</body>
</html>

View File

@@ -1,14 +0,0 @@
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="mb-4">
<i class="fas fa-exclamation-circle fa-5x" style="color: var(--primary-color);"></i>
</div>
<h1 class="display-4 mb-4">Đã xảy ra lỗi</h1>
<p class="lead mb-4"><%= message %></p>
<a href="/" class="btn btn-primary">
<i class="fas fa-home me-2"></i>Quay lại trang chủ
</a>
</div>
</div>
</div>

View File

@@ -1,98 +0,0 @@
<div class="container my-5">
<h1>🧪 Test Footer Implementation</h1>
<p>Trang này test việc render footer từ backend API trong layout chính</p>
<div class="alert alert-info">
<h5>📊 Kiểm tra các yếu tố:</h5>
<ul class="mb-0">
<li>✅ Footer được load từ <code>GET /api/footer</code></li>
<li>✅ Render đúng các section: About, Columns, Contact, Social</li>
<li>✅ Tôn trọng thứ tự (order) của columns và social links</li>
<li>✅ XSS protection với escapeHtml()</li>
<li>✅ Fallback footer nếu API lỗi</li>
</ul>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>🔗 API Test</h5>
</div>
<div class="card-body">
<button class="btn btn-primary" onclick="testFooterAPI()">Test API</button>
<div id="apiResult" class="mt-3"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>📱 Footer Status</h5>
</div>
<div class="card-body">
<div id="footerStatus">
<p>Footer sẽ được load tự động khi trang tải xong.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
async function testFooterAPI() {
const resultDiv = document.getElementById("apiResult");
resultDiv.innerHTML = "<p>⏳ Đang test API...</p>";
try {
const response = await fetch("/api/footer");
const data = await response.json();
let html = '<div class="alert alert-success">';
html += "<h6>✅ API Response thành công:</h6>";
html += `<p><strong>About:</strong> ${data.about?.title || "N/A"}</p>`;
html += `<p><strong>Columns:</strong> ${data.columns?.length || 0} cột</p>`;
html += `<p><strong>Social:</strong> ${data.social?.links?.length || 0} links</p>`;
html += `<p><strong>Contact:</strong> ${data.contact?.email || "N/A"}</p>`;
html += "</div>";
resultDiv.innerHTML = html;
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Lỗi: ${error.message}</div>`;
}
}
// Monitor footer loading
document.addEventListener("DOMContentLoaded", function () {
const statusDiv = document.getElementById("footerStatus");
// Check if footer elements exist
const footerContent = document.getElementById("footerContent");
const footerCopyright = document.getElementById("footerCopyright");
if (footerContent && footerCopyright) {
statusDiv.innerHTML = `
<div class="alert alert-success">
✅ Footer elements found<br>
✅ Dynamic loading script active<br>
✅ Footer sẽ được render từ API
</div>
`;
// Monitor footer content changes
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.target.id === "footerContent" && mutation.target.innerHTML.includes("About")) {
statusDiv.innerHTML +=
'<div class="alert alert-info mt-2">🎉 Footer đã được render thành công!</div>';
}
});
});
observer.observe(footerContent, { childList: true, subtree: true });
} else {
statusDiv.innerHTML = '<div class="alert alert-warning">⚠️ Footer elements không tìm thấy</div>';
}
});
</script>