forked from UKSOURCE/cms.hailearning.edu.vn
fix: enhance FAQ management and add delete confirmation modal
This commit is contained in:
BIN
public/uploads/service/Learning.jpg
Normal file
BIN
public/uploads/service/Learning.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 464 KiB |
@@ -2,7 +2,6 @@ const Service = require("../models/service");
|
||||
|
||||
const getServiceData = async () => {
|
||||
const service = await Service.findOne().sort({ updatedAt: -1 });
|
||||
console.log("check layout", service.services.items.layout);
|
||||
|
||||
if (!service) {
|
||||
return {
|
||||
|
||||
@@ -249,9 +249,9 @@
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID</label>
|
||||
<label class="form-label">ID (Auto-generated)</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="<%= faq.id || 'faq-' + (index + 1) %>" required>
|
||||
value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
@@ -399,20 +399,22 @@ function removeFeature(button) {
|
||||
|
||||
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 ${faqIndex + 1}</h6>
|
||||
<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</label>
|
||||
<label class="form-label">ID (Auto-generated)</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="faq-${faqIndex + 1}" required>
|
||||
value="${newFaqId}" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
@@ -441,13 +443,43 @@ function addFAQ() {
|
||||
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 = {
|
||||
|
||||
@@ -283,6 +283,43 @@
|
||||
</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;
|
||||
@@ -534,11 +571,110 @@ function addService() {
|
||||
}
|
||||
|
||||
function deleteService(index) {
|
||||
if (confirm('Are you sure you want to delete this service? This action cannot be undone.')) {
|
||||
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();
|
||||
showSuccess('Service deleted successfully!');
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,4 +915,188 @@ function showError(message) {
|
||||
.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>
|
||||
@@ -722,11 +722,10 @@
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/about' || currentPath === '/admin/affiliations' || currentPath === '/admin/partnerships' ? 'active' : '' %>"
|
||||
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/about-us' || currentPath === '/admin/safety' || currentPath === '/admin/faq' || currentPath === '/admin/insurance' || currentPath === '/admin/travel' || currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
|
||||
href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
About
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
|
||||
@@ -737,13 +736,13 @@
|
||||
href="/admin/safety">Safety</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/FAQ' ? 'active' : '' %>" href="/admin/faq">FAQ</a>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/faq' ? 'active' : '' %>"
|
||||
href="/admin/faq">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/insurance' ? 'active' : '' %>"
|
||||
href="/admin/insurance">Insurance</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
|
||||
href="/admin/travel">Travel</a>
|
||||
@@ -752,12 +751,27 @@
|
||||
<a class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
|
||||
href="/admin/terms-conditions">Terms & Conditions</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle <%= currentPath === '/admin/service' || currentPath === '/admin/activity' ? 'active' : '' %>"
|
||||
href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Services
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
|
||||
href="/admin/service">Service</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item <%= currentPath === '/admin/activity' ? 'active' : '' %>"
|
||||
href="/admin/activity">Activity & Booking</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
|
||||
Us</a>
|
||||
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
|
||||
href="/admin/contact">Contact Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>"
|
||||
@@ -771,60 +785,6 @@
|
||||
<a class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
|
||||
href="/admin/camp-location">Camp Location</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>" href="/admin/activity">Activity
|
||||
& Booking</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item <%= currentPath === '/admin/travel' ? 'active' : '' %>"
|
||||
href="/admin/travel"
|
||||
>Travel</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item <%= currentPath === '/admin/terms-conditions' ? 'active' : '' %>"
|
||||
href="/admin/terms-conditions"
|
||||
>Terms & Conditions</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
|
||||
href="/admin/service"
|
||||
>Service</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>"
|
||||
href="/admin/contact"
|
||||
>Contact Us</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link <%= currentPath === '/admin/camp-location' ? 'active' : '' %>"
|
||||
href="/admin/camp-location"
|
||||
>Camp Location</a
|
||||
>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link <%= currentPath === '/admin/activity' ? 'active' : '' %>"
|
||||
href="/admin/activity"
|
||||
>Activity & Booking</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</ul>
|
||||
<div class="d-flex align-items-center">
|
||||
<% if (locals.user) { %>
|
||||
|
||||
Reference in New Issue
Block a user