Files
uldp-degree-mangement-system/views/admin/service/index.ejs

1102 lines
38 KiB
Plaintext

<div class="container">
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
<div>
<h1 class="h3 mb-0" style="color: var(--primary-dark);"><%= title %></h1>
<p class="text-muted mb-0">Manage services and their detailed content</p>
</div>
<div>
<a href="<%= frontendUrl %>/admin/service/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Services Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form action="/admin/service/update" method="POST" class="content-with-fixed-buttons" id="serviceForm">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="services" id="servicesJson">
<!-- 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="#services-list" role="tab">
<i class="fas fa-list me-2"></i>Services List
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#section-settings" role="tab">
<i class="fas fa-cog me-2"></i>Section Settings
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#add-service" role="tab">
<i class="fas fa-plus me-2"></i>Add Service
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Services List Tab -->
<div class="tab-pane fade show active" id="services-list" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="fas fa-cogs text-primary"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0">Total Services</h6>
<h3 class="mb-0" id="totalCount">
<%= (data.services && data.services.items) ? data.services.items.length : 0 %>
</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="fas fa-align-left text-success"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0">Left Layout</h6>
<h3 class="mb-0" id="leftCount">
<%= (data.services && data.services.items) ? data.services.items.filter(s => s && s.layout === 'left').length : 0 %>
</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="fas fa-align-right text-info"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0">Right Layout</h6>
<h3 class="mb-0" id="rightCount">
<%= (data.services && data.services.items) ? data.services.items.filter(s => s && s.layout === 'right').length : 0 %>
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Services Table -->
<div class="table-responsive">
<table class="table table-hover mb-0" id="servicesTable">
<thead class="table-light">
<tr>
<th style="width: 40px;">#</th>
<th style="width: 80px;">Image</th>
<th>Name</th>
<th>Slug</th>
<th style="width: 100px;">Layout</th>
<th style="width: 200px;">Actions</th>
</tr>
</thead>
<tbody id="servicesTableBody">
<% if (data.services && data.services.items) { %>
<% data.services.items.forEach((service, index) => { %>
<% if (service) { %>
<tr data-index="<%= index %>">
<td class="text-muted"><%= index + 1 %></td>
<td>
<% if (service.image) { %>
<img src="<%= getFullImageUrl(service.image) %>" class="rounded" style="width: 60px; height: 45px; object-fit: cover;" alt="<%= service.name %>" >
<% } else { %>
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 60px; height: 45px;">
<i class="fas fa-image text-muted"></i>
</div>
<% } %>
</td>
<td>
<strong><%= service.name %></strong>
<% if (service.description) { %>
<br><small class="text-muted"><%= service.description.substring(0, 50) %>...</small>
<% } %>
</td>
<td>
<span class="badge bg-light text-dark"><%= service.slug %></span>
</td>
<td>
<span class="badge bg-<%= service.layout === 'left' ? 'primary' : 'success' %>">
<i class="fas fa-align-<%= service.layout %>"></i> <%= service.layout %>
</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="/admin/service/<%= service.slug %>/edit" class="btn btn-sm btn-outline-primary" title="Edit Service">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/service/<%= service.slug %>/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
<i class="fas fa-cog"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(<%= index %>)" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% } %>
<% }) %>
<% } %>
</tbody>
</table>
</div>
<% if (!data.services || !data.services.items || data.services.items.length === 0) { %>
<div class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No services found</h5>
<p class="text-muted">Click "Add Service" tab to create your first service.</p>
</div>
<% } %>
</div>
</div>
</div>
<!-- Section Settings Tab -->
<div class="tab-pane fade" id="section-settings" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Services Section Title</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Sub Title</label>
<input type="text" class="form-control" id="servicesSubTitle"
value="<%= data.services?.title?.subTitle || '' %>"
placeholder="Enter subtitle for services section">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Main Title</label>
<input type="text" class="form-control" id="servicesMainTitle"
value="<%= data.services?.title?.mainTitle || '' %>"
placeholder="Enter main title for services section">
</div>
</div>
</div>
</div>
</div>
<!-- Add Service Tab -->
<div class="tab-pane fade" id="add-service" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Add New Service</h6>
<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="newServiceName" placeholder="Enter service name">
</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-success ms-1" style="font-size: 0.7em;">AUTO</span>
</label>
<div class="input-group">
<input type="text" class="form-control" id="newServiceSlug" placeholder="Click generate to create 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. Generated automatically from the service name.</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Layout</label>
<select class="form-control" id="newServiceLayout">
<option value="left">Left</option>
<option value="right">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="newServiceImage" placeholder="Enter image path">
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="newServiceImage" data-image-type="service">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
<div class="col-md-4">
<div id="newServiceImagePreview"></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="newServiceDescription" rows="3" placeholder="Enter service description"></textarea>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-primary" onclick="addService()">
<i class="fas fa-plus me-2"></i>Add Service
</button>
</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>
<!-- Custom Delete Confirmation Modal -->
<div id="customDeleteModal" class="custom-modal" style="display: none;">
<div class="custom-modal-backdrop"></div>
<div class="custom-modal-dialog">
<div class="custom-modal-content">
<div class="custom-modal-header">
<h5 class="custom-modal-title">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>Confirm Delete
</h5>
<button type="button" class="custom-modal-close" onclick="closeDeleteModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="custom-modal-body">
<div class="text-center mb-3">
<div class="delete-icon">
<i class="fas fa-trash fa-2x text-danger"></i>
</div>
</div>
<h6 class="text-center mb-3">Delete Service</h6>
<p class="text-center text-muted mb-0">
Are you sure you want to delete "<strong id="deleteServiceName"></strong>"?
<br><small class="text-danger">This action cannot be undone.</small>
</p>
</div>
<div class="custom-modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">
<i class="fas fa-times me-2"></i>Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
<i class="fas fa-trash me-2"></i>Delete Service
</button>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script>
let originalFormData = null;
let servicesData = <%- JSON.stringify(data.services?.items || []) %>;
document.addEventListener('DOMContentLoaded', function() {
// Initialize form data
originalFormData = <%- JSON.stringify(data) %>;
// Set initial JSON values
updateAllJsonInputs(originalFormData);
// Initialize form handlers
initializeFormHandlers();
});
function initializeFormHandlers() {
// Form submission
const form = document.getElementById('serviceForm');
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);
});
});
// Image preview for new service
const newServiceImageInput = document.getElementById('newServiceImage');
if (newServiceImageInput) {
newServiceImageInput.addEventListener('input', function() {
updateImagePreview('newServiceImagePreview', this.value);
});
}
// Generate slug from service name
const newServiceNameInput = document.getElementById('newServiceName');
const newServiceSlugInput = document.getElementById('newServiceSlug');
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
const generateSlugBtn = document.getElementById('generateSlugBtn');
if (newServiceNameInput && newServiceSlugInput && generateSlugBtn) {
// Generate slug button
generateSlugBtn.addEventListener('click', async function() {
const serviceName = newServiceNameInput.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);
// Check if we're in edit mode
const addBtn = document.querySelector('#add-service .btn-primary');
const isEditMode = addBtn && addBtn.textContent.includes('Update');
// Get current service index if editing
let currentIndex = -1;
if (isEditMode) {
const onclickAttr = addBtn.getAttribute('onclick');
const match = onclickAttr.match(/updateService\((\d+)\)/);
if (match) {
currentIndex = parseInt(match[1]);
}
}
// Check for duplicate slug
if (isSlugDuplicate(slug, currentIndex)) {
const uniqueSlug = generateUniqueSlug(slug);
newServiceSlugInput.value = uniqueSlug;
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
} else {
newServiceSlugInput.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.');
}
});
}
}
// 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;
}
// 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 (excluding current service when editing)
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 = '';
}
}
function addService() {
const name = document.getElementById('newServiceName').value.trim();
const slug = document.getElementById('newServiceSlug').value.trim();
const description = document.getElementById('newServiceDescription').value.trim();
const image = document.getElementById('newServiceImage').value.trim();
const layout = document.getElementById('newServiceLayout').value;
if (!name || !slug || !description) {
showError('Please fill in all required fields and generate a slug.');
return;
}
// Check if service with same slug already exists
if (isSlugDuplicate(slug, -1)) {
showError('Service with this slug already exists. Please generate a new slug.');
return;
}
const newService = {
slug: slug,
name: name,
description: description,
image: image,
layout: layout,
details: {
title: name,
description: description,
mainImage: "img/inner-page/service-details/details-1.jpg",
overviewTitle: "Service Overview",
overviewDescription: "Service overview description...",
additionalDescription: "Additional description...",
keyFeaturesTitle: "Key Features",
keyFeaturesImage: "img/inner-page/service-details/details-2.jpg",
features: [],
faqTitle: "Frequently Asked Question",
faqImage: "img/inner-page/service-details/details-3.jpg",
faq: []
}
};
servicesData.push(newService);
updateServicesTable();
updateStatistics();
// Clear form
clearAddServiceForm();
showSuccess('Service added successfully!');
}
function deleteService(index) {
const service = servicesData[index];
if (!service) return;
// Set service name in modal
document.getElementById('deleteServiceName').textContent = service.name || 'Unnamed Service';
// Show custom modal
showDeleteModal();
// Handle confirm delete
const confirmBtn = document.getElementById('confirmDeleteBtn');
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
newConfirmBtn.addEventListener('click', async function() {
// Show loading state
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Deleting...';
// Disable cancel button and close button during delete
const cancelBtn = document.querySelector('.custom-modal .btn-secondary');
const closeBtn = document.querySelector('.custom-modal-close');
cancelBtn.disabled = true;
closeBtn.disabled = true;
// Add loading overlay to modal
showModalLoading();
try {
// Simulate API call delay for better UX
await new Promise(resolve => setTimeout(resolve, 1000));
// Perform delete
servicesData.splice(index, 1);
updateServicesTable();
updateStatistics();
// Hide modal
closeDeleteModal();
// Show success message
showSuccess(`Service "${service.name}" deleted successfully!`);
} catch (error) {
console.error('Error deleting service:', error);
showError('Failed to delete service. Please try again.');
} finally {
// Reset button states
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
cancelBtn.disabled = false;
closeBtn.disabled = false;
// Hide loading overlay
hideModalLoading();
}
});
}
function showModalLoading() {
const modal = document.getElementById('customDeleteModal');
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'modal-loading-overlay';
loadingOverlay.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin fa-2x text-primary"></i>
<p class="mt-2 mb-0">Deleting service...</p>
</div>
`;
modal.appendChild(loadingOverlay);
}
function hideModalLoading() {
const loadingOverlay = document.querySelector('.modal-loading-overlay');
if (loadingOverlay) {
loadingOverlay.remove();
}
}
function showDeleteModal() {
const modal = document.getElementById('customDeleteModal');
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Add animation
setTimeout(() => {
modal.classList.add('show');
}, 10);
// Close modal when clicking backdrop (only if not loading)
const backdrop = modal.querySelector('.custom-modal-backdrop');
backdrop.onclick = function() {
if (!document.querySelector('.modal-loading-overlay')) {
closeDeleteModal();
}
};
// Close modal with ESC key (only if not loading)
document.addEventListener('keydown', handleEscKey);
}
function handleEscKey(event) {
if (event.key === 'Escape' && !document.querySelector('.modal-loading-overlay')) {
closeDeleteModal();
}
}
function clearAddServiceForm() {
document.getElementById('newServiceName').value = '';
document.getElementById('newServiceSlug').value = '';
document.getElementById('newServiceDescription').value = '';
document.getElementById('newServiceImage').value = '';
document.getElementById('newServiceLayout').value = 'left';
document.getElementById('newServiceImagePreview').innerHTML = '';
// Reset slug indicator
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
if (slugAutoIndicator) {
slugAutoIndicator.textContent = 'AUTO';
slugAutoIndicator.className = 'badge bg-success ms-1';
slugAutoIndicator.style.fontSize = '0.7em';
}
}
function updateServicesTable() {
const tbody = document.getElementById('servicesTableBody');
tbody.innerHTML = '';
servicesData.forEach((service, index) => {
if (!service) return; // Skip undefined services
let imageHtml = '';
if (service.image) {
const fullImageUrl = getFullImageUrlJS(service.image);
imageHtml = `<img src="${fullImageUrl}" class="rounded" style="width: 60px; height: 45px; object-fit: cover;" alt="${service.name || 'Service'}" onerror="this.onerror=null; this.src='/images/placeholder.png'; this.classList.add('bg-light');">`;
} else {
imageHtml = `<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 60px; height: 45px;"><i class="fas fa-image text-muted"></i></div>`;
}
const row = `
<tr data-index="${index}">
<td class="text-muted">${index + 1}</td>
<td>${imageHtml}</td>
<td>
<strong>${service.name || 'Unnamed Service'}</strong>
${service.description ? `<br><small class="text-muted">${service.description.substring(0, 50)}...</small>` : ''}
</td>
<td><span class="badge bg-light text-dark">${service.slug || 'no-slug'}</span></td>
<td><span class="badge bg-${(service.layout === 'left') ? 'primary' : 'success'}"><i class="fas fa-align-${service.layout || 'left'}"></i> ${service.layout || 'left'}</span></td>
<td>
<div class="btn-group" role="group">
<a href="/admin/service/${service.slug || 'unknown'}/edit" class="btn btn-sm btn-outline-primary" title="Edit Service">
<i class="fas fa-edit"></i>
</a>
<a href="/admin/service/${service.slug || 'unknown'}/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
<i class="fas fa-cog"></i>
</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(${index})" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
tbody.innerHTML += row;
});
}
function updateStatistics() {
const validServices = servicesData.filter(s => s && s.layout);
document.getElementById('totalCount').textContent = servicesData.length;
document.getElementById('leftCount').textContent = validServices.filter(s => s.layout === 'left').length;
document.getElementById('rightCount').textContent = validServices.filter(s => s.layout === 'right').length;
}
function updateAllJsonInputs(data) {
const servicesSection = {
title: data.services?.title || { subTitle: '', mainTitle: '' },
items: data.services?.items || []
};
document.getElementById('servicesJson').value = JSON.stringify(servicesSection);
}
function updateJsonData() {
const servicesSection = {
title: {
subTitle: document.getElementById('servicesSubTitle').value,
mainTitle: document.getElementById('servicesMainTitle').value,
},
items: servicesData
};
// Gửi toàn bộ services section (bao gồm cả title và items)
document.getElementById('servicesJson').value = JSON.stringify(servicesSection);
}
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...';
}
// Sử dụng imageType=service để upload vào folder service
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) || document.querySelector(`[name="${targetInput}"]`);
if (!input) throw new Error('Target input not found');
input.value = result.path;
// Update preview if it's the new service image
if (targetInput === 'newServiceImage') {
updateImagePreview('newServiceImagePreview', 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 showWarning(message) {
// Create and show warning alert
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) {
// 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;
}
/* Custom Delete Modal Styles */
.custom-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.custom-modal.show {
opacity: 1;
}
.custom-modal-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
}
.custom-modal-dialog {
position: relative;
z-index: 10001;
max-width: 400px;
width: 90%;
transform: scale(0.8);
transition: transform 0.3s ease;
}
.custom-modal.show .custom-modal-dialog {
transform: scale(1);
}
.custom-modal-content {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.custom-modal-header {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
padding: 20px 25px 15px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: between;
align-items: center;
}
.custom-modal-title {
color: var(--primary-dark);
font-weight: 600;
margin: 0;
flex: 1;
}
.custom-modal-close {
background: none;
border: none;
font-size: 18px;
color: #6c757d;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.custom-modal-close:hover {
background: rgba(0, 0, 0, 0.1);
color: #495057;
}
.custom-modal-body {
padding: 25px;
text-align: center;
}
.delete-icon {
background: rgba(220, 53, 69, 0.1);
border-radius: 50%;
width: 80px;
height: 80px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.custom-modal-footer {
padding: 15px 25px 25px;
display: flex;
gap: 10px;
justify-content: center;
}
.custom-modal-footer .btn {
min-width: 120px;
}
.custom-modal .btn-danger {
background: linear-gradient(135deg, #dc3545, #c82333);
border: none;
transition: all 0.3s ease;
}
.custom-modal .btn-danger:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
}
.custom-modal .btn-secondary {
background: #6c757d;
border: none;
transition: all 0.3s ease;
}
.custom-modal .btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
/* Modal Loading Overlay */
.modal-loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 10002;
border-radius: 15px;
backdrop-filter: blur(2px);
}
.loading-spinner {
text-align: center;
color: var(--primary-color);
}
.loading-spinner p {
font-size: 14px;
color: #6c757d;
font-weight: 500;
}
.loading-spinner i {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Disable buttons during loading */
.custom-modal .btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.custom-modal-close:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
</style>