Merge pull request 'fix/bao-04022026-1625-service-management' (#17) from fix/bao-04022026-1625-service-management into main

Reviewed-on: UKSOURCE/cms.hailearning.edu.vn#17
This commit is contained in:
2026-02-04 09:27:48 +00:00
4 changed files with 131 additions and 49 deletions

View File

@@ -149,7 +149,7 @@
</div>
<!-- Features List -->
<div class="row">
<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>
@@ -228,7 +228,7 @@
</div>
<!-- FAQ List -->
<div class="row">
<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>

View File

@@ -156,7 +156,7 @@
<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">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService('<%= service.slug %>')" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -570,18 +570,32 @@ function addService() {
showSuccess('Service added successfully!');
}
function deleteService(index) {
const service = servicesData[index];
if (!service) return;
function deleteService(slug) {
const serviceIndex = servicesData.findIndex(service => service && service.slug === slug);
if (serviceIndex === -1) {
showError('Service not found.');
return;
}
const service = servicesData[serviceIndex];
// Set service name in modal
document.getElementById('deleteServiceName').textContent = service.name || 'Unnamed Service';
const serviceNameElement = document.getElementById('deleteServiceName');
if (serviceNameElement) {
serviceNameElement.textContent = service.name || 'Unnamed Service';
}
// Show custom modal
showDeleteModal();
// Handle confirm delete
const confirmBtn = document.getElementById('confirmDeleteBtn');
if (!confirmBtn) {
showError('Delete button not found.');
return;
}
// Remove any existing event listeners by cloning the button
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
@@ -593,36 +607,54 @@ function deleteService(index) {
// 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;
if (cancelBtn) cancelBtn.disabled = true;
if (closeBtn) closeBtn.disabled = true;
// Add loading overlay to modal
showModalLoading();
try {
// Simulate API call delay for better UX
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => setTimeout(resolve, 800));
// Check if service still exists (in case of concurrent modifications)
const currentServiceIndex = servicesData.findIndex(service => service && service.slug === slug);
if (currentServiceIndex === -1) {
throw new Error('Service no longer exists.');
}
// Store service name for success message
const serviceName = servicesData[currentServiceIndex].name || 'Unnamed Service';
// Perform delete
servicesData.splice(index, 1);
servicesData.splice(currentServiceIndex, 1);
updateServicesTable();
updateStatistics();
// Hide loading overlay first
hideModalLoading();
// Reset button states
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
if (cancelBtn) cancelBtn.disabled = false;
if (closeBtn) closeBtn.disabled = false;
// Hide modal
closeDeleteModal();
// Show success message
showSuccess(`Service "${service.name}" deleted successfully!`);
showSuccess(`Service "${serviceName}" deleted successfully!`);
} catch (error) {
console.error('Error deleting service:', error);
showError('Failed to delete service. Please try again.');
} finally {
// Reset button states
// Reset button states on error
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Service';
cancelBtn.disabled = false;
closeBtn.disabled = false;
if (cancelBtn) cancelBtn.disabled = false;
if (closeBtn) closeBtn.disabled = false;
// Hide loading overlay
hideModalLoading();
@@ -632,6 +664,11 @@ function deleteService(index) {
function showModalLoading() {
const modal = document.getElementById('customDeleteModal');
if (!modal) return;
// Remove any existing loading overlay first
hideModalLoading();
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'modal-loading-overlay';
loadingOverlay.innerHTML = `
@@ -640,7 +677,11 @@ function showModalLoading() {
<p class="mt-2 mb-0">Deleting service...</p>
</div>
`;
modal.appendChild(loadingOverlay);
const modalContent = modal.querySelector('.custom-modal-content');
if (modalContent) {
modalContent.appendChild(loadingOverlay);
}
}
function hideModalLoading() {
@@ -650,8 +691,36 @@ function hideModalLoading() {
}
}
function closeDeleteModal() {
const modal = document.getElementById('customDeleteModal');
if (!modal) return;
// Check if loading - don't close if loading
if (document.querySelector('.modal-loading-overlay')) {
return;
}
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
document.body.style.overflow = '';
}, 300);
// Remove ESC key listener
document.removeEventListener('keydown', handleEscKey);
// Clean up any existing event listeners
const backdrop = modal.querySelector('.custom-modal-backdrop');
if (backdrop) {
backdrop.onclick = null;
}
}
function showDeleteModal() {
const modal = document.getElementById('customDeleteModal');
if (!modal) return;
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
@@ -662,11 +731,14 @@ function showDeleteModal() {
// 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();
}
};
if (backdrop) {
backdrop.onclick = function(e) {
// Only close if clicking the backdrop itself, not its children
if (e.target === backdrop && !document.querySelector('.modal-loading-overlay')) {
closeDeleteModal();
}
};
}
// Close modal with ESC key (only if not loading)
document.addEventListener('keydown', handleEscKey);
@@ -728,7 +800,7 @@ function updateServicesTable() {
<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">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService('${service.slug || 'unknown'}')" title="Delete Service">
<i class="fas fa-trash"></i>
</button>
</div>
@@ -963,6 +1035,7 @@ function showError(message) {
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
position: relative;
}
.custom-modal-header {
@@ -970,7 +1043,7 @@ function showError(message) {
padding: 20px 25px 15px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: between;
justify-content: space-between;
align-items: center;
}
@@ -995,13 +1068,20 @@ function showError(message) {
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.custom-modal-close:hover {
.custom-modal-close:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.1);
color: #495057;
}
.custom-modal-close:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.custom-modal-body {
padding: 25px;
text-align: center;
@@ -1094,9 +1174,12 @@ function showError(message) {
transform: none !important;
}
.custom-modal-close:disabled {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
/* Ensure modal content has relative positioning for overlay */
.custom-modal-content {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
position: relative;
}
</style>