forked from UKSOURCE/cms.hailearning.edu.vn
1684 lines
101 KiB
Plaintext
1684 lines
101 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 camp activities and programs</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="<%= frontendUrl %>/activities/" class="btn btn-outline-primary" target="_blank">
|
|
<i class="fas fa-external-link-alt me-2"></i>View Activities Page
|
|
</a>
|
|
<a href="/admin/activity/create" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add Activity
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs Navigation -->
|
|
<ul class="nav nav-tabs mb-4" id="activityTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="list-tab" data-bs-toggle="tab" data-bs-target="#list-tab-pane"
|
|
type="button" role="tab" aria-controls="list-tab-pane" aria-selected="true">
|
|
<i class="fas fa-list me-2"></i>Activities List
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="filters-tab" data-bs-toggle="tab" data-bs-target="#filters-tab-pane"
|
|
type="button" role="tab" aria-controls="filters-tab-pane" aria-selected="false">
|
|
<i class="fas fa-filter me-2"></i>Filter Settings
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="bookings-tab" data-bs-toggle="tab" data-bs-target="#bookings-tab-pane"
|
|
type="button" role="tab" aria-controls="bookings-tab-pane" aria-selected="false">
|
|
<i class="fas fa-calendar-check me-2"></i>All Bookings
|
|
<span class="badge bg-primary ms-1"><%= allBookingsStats ? allBookingsStats.total : 0 %></span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content" id="activityTabsContent">
|
|
<!-- Activities List Tab -->
|
|
<div class="tab-pane fade show active" id="list-tab-pane" role="tabpanel" aria-labelledby="list-tab"
|
|
tabindex="0">
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<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-running text-primary"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<h6 class="text-muted mb-0">Total Activities</h6>
|
|
<h3 class="mb-0" id="totalCount">
|
|
<%= pagination.total %>
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<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-check-circle text-success"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<h6 class="text-muted mb-0">Active</h6>
|
|
<h3 class="mb-0" id="activeCount">
|
|
<%= typeof activeCount !== 'undefined' ? activeCount : (items.filter(i => i.isActive).length) %>
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<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-warning bg-opacity-10 rounded-circle p-3">
|
|
<i class="fas fa-pause-circle text-warning"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<h6 class="text-muted mb-0">Inactive</h6>
|
|
<h3 class="mb-0" id="inactiveCount">
|
|
<%= (typeof activeCount !== 'undefined' && typeof pagination !== 'undefined') ? (pagination.total - activeCount) : items.filter(i => !i.isActive).length %>
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<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-file-alt text-info"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1 ms-3">
|
|
<h6 class="text-muted mb-0">Pages</h6>
|
|
<h3 class="mb-0">
|
|
<%= pagination.totalPages %>
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hero Section Info -->
|
|
<% if (items && items.length > 0 && items[0].hero) { %>
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-header bg-white border-bottom">
|
|
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Hero Section</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="heroInlineForm" method="POST" action="/admin/activity/hero/update" class="content-with-fixed-buttons">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6 class="mb-2">Activities Hero</h6>
|
|
<label class="form-label fw-medium">Banner Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="bannerImageActivities" name="bannerImageActivities" value="<%= items[0].hero.bannerImageActivities || items[0].hero.bannerImage || '' %>">
|
|
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="bannerImageActivities" data-image-type="activity">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<label class="form-label fw-medium">Title</label>
|
|
<input type="text" class="form-control" id="titleActivities" name="titleActivities" value="<%= items[0].hero.titleActivities || items[0].hero.title || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6 class="mb-2">Booking Hero</h6>
|
|
<label class="form-label fw-medium">Banner Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="bannerImageBooking" name="bannerImageBooking" value="<%= items[0].hero.bannerImageBooking || items[0].hero.bannerImage || '' %>">
|
|
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="bannerImageBooking" data-image-type="activity">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<label class="form-label fw-medium">Title</label>
|
|
<input type="text" class="form-control" id="titleBooking" name="titleBooking" value="<%= items[0].hero.titleBooking || items[0].hero.title || '' %>">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-3">
|
|
<div class="col-md-6">
|
|
<div id="previewActivities">
|
|
<% if (items[0].hero && (items[0].hero.bannerImageActivities || items[0].hero.bannerImage)) { %>
|
|
<img src="<%= items[0].hero.bannerImageActivities || items[0].hero.bannerImage %>" class="img-thumbnail uploaded-preview" style="height:200px; width:100%; object-fit:cover;" alt="Activities banner preview">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div id="previewBooking">
|
|
<% if (items[0].hero && (items[0].hero.bannerImageBooking || items[0].hero.bannerImage)) { %>
|
|
<img src="<%= items[0].hero.bannerImageBooking || items[0].hero.bannerImage %>" class="img-thumbnail uploaded-preview" style="height:200px; width:100%; object-fit:cover;" alt="Booking banner preview">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fixed Bottom Buttons -->
|
|
<div class="fixed-bottom-buttons">
|
|
<button type="button" class="btn btn-secondary" id="heroResetBtn">
|
|
<i class="fas fa-undo me-2"></i>Reset
|
|
</button>
|
|
<button type="submit" class="btn btn-primary" id="heroSubmitBtn">
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function(){
|
|
const form = document.getElementById('heroInlineForm');
|
|
|
|
if (form) {
|
|
form.addEventListener('submit', function(e) {
|
|
const submitBtn = document.getElementById('heroSubmitBtn');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
|
}
|
|
});
|
|
|
|
// Initialize upload buttons inside this form
|
|
form.querySelectorAll('.btn-upload-image').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const targetInput = this.dataset.targetInput;
|
|
const imageType = this.dataset.imageType || 'activity';
|
|
openImageUploader(targetInput, imageType).catch(err => console.error('Upload error', err));
|
|
});
|
|
});
|
|
|
|
// Reset button
|
|
const resetBtn = document.getElementById('heroResetBtn');
|
|
if (resetBtn) {
|
|
resetBtn.addEventListener('click', function () {
|
|
document.getElementById('titleActivities').value = '<%= (items[0].hero && (items[0].hero.titleActivities || items[0].hero.title)) ? (items[0].hero.titleActivities || items[0].hero.title).replace(/'/g, "\\'") : '' %>';
|
|
document.getElementById('titleBooking').value = '<%= (items[0].hero && (items[0].hero.titleBooking || items[0].hero.title)) ? (items[0].hero.titleBooking || items[0].hero.title).replace(/'/g, "\\'") : '' %>';
|
|
document.getElementById('bannerImageActivities').value = '<%= (items[0].hero && (items[0].hero.bannerImageActivities || items[0].hero.bannerImage)) ? items[0].hero.bannerImageActivities || items[0].hero.bannerImage : '' %>';
|
|
document.getElementById('bannerImageBooking').value = '<%= (items[0].hero && (items[0].hero.bannerImageBooking || items[0].hero.bannerImage)) ? items[0].hero.bannerImageBooking || items[0].hero.bannerImage : '' %>';
|
|
|
|
// update previews
|
|
const previewA = document.getElementById('previewActivities');
|
|
const previewB = document.getElementById('previewBooking');
|
|
previewA.innerHTML = '';
|
|
previewB.innerHTML = '';
|
|
const aUrl = '<%= (items[0].hero && (items[0].hero.bannerImageActivities || items[0].hero.bannerImage)) ? (items[0].hero.bannerImageActivities || items[0].hero.bannerImage) : '' %>';
|
|
const bUrl = '<%= (items[0].hero && (items[0].hero.bannerImageBooking || items[0].hero.bannerImage)) ? (items[0].hero.bannerImageBooking || items[0].hero.bannerImage) : '' %>';
|
|
if (aUrl) previewA.innerHTML = `<img src="${aUrl}" class="img-thumbnail uploaded-preview" style="height:200px; width:100%; object-fit:cover;">`;
|
|
if (bUrl) previewB.innerHTML = `<img src="${bUrl}" class="img-thumbnail uploaded-preview" style="height:200px; width:100%; object-fit:cover;">`;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Image uploader used across admin views
|
|
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');
|
|
|
|
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://'))) ? result.path : (window.location.origin + result.path);
|
|
input.value = result.path;
|
|
|
|
// update matching preview container
|
|
if (targetInput === 'bannerImageActivities') {
|
|
const p = document.getElementById('previewActivities');
|
|
if (p) p.innerHTML = `<img src="${previewUrl}" class="img-thumbnail uploaded-preview" style="height:200px; width:100%; object-fit:cover;">`;
|
|
} else if (targetInput === 'bannerImageBooking') {
|
|
const p = document.getElementById('previewBooking');
|
|
if (p) p.innerHTML = `<img src="${previewUrl}" class="img-thumbnail uploaded-preview" style="height:200px; width:100%; object-fit:cover;">`;
|
|
}
|
|
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.innerHTML = originalBtnHtml;
|
|
}
|
|
|
|
resolve(result);
|
|
} catch (err) {
|
|
if (uploadBtn) {
|
|
uploadBtn.disabled = false;
|
|
uploadBtn.innerHTML = originalBtnHtml;
|
|
}
|
|
console.error('openImageUploader error', err);
|
|
alert('Upload failed: ' + (err.message || 'Unknown'));
|
|
reject(err);
|
|
} finally {
|
|
fileInput.remove();
|
|
}
|
|
};
|
|
|
|
fileInput.click();
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
<% } %>
|
|
|
|
<!-- Activities Table -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Activities List</h5>
|
|
<div class="d-flex gap-2">
|
|
<input type="text" class="form-control form-control-sm" id="searchInput"
|
|
placeholder="Search activities..." style="width: 200px;">
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<% if (items && items.length> 0) { %>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0" id="activitiesTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width: 40px;">#</th>
|
|
<th style="width: 80px;">Image</th>
|
|
<th>Name</th>
|
|
<th>Price</th>
|
|
<th>Season</th>
|
|
<th>Locations</th>
|
|
<th>Rating</th>
|
|
<th style="width: 100px;">Bookings</th>
|
|
<th style="width: 80px;">Status</th>
|
|
<th style="width: 140px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="activitiesBody">
|
|
<% items.forEach((item, index) => { %>
|
|
<tr data-id="<%= item._id %>" class="activity-row">
|
|
<td class="text-muted">
|
|
<%= (pagination.page - 1) * pagination.limit + index + 1 %>
|
|
</td>
|
|
<td>
|
|
<% if (item.image) { %>
|
|
<% let imgSrc = item.image; if (!imgSrc.startsWith('http') &&
|
|
!imgSrc.startsWith('/')) { imgSrc = '/uploads/' + imgSrc; } %>
|
|
<img src="<%= imgSrc %>" class="rounded"
|
|
style="width: 60px; height: 45px; object-fit: cover;"
|
|
alt="<%= item.name %>"
|
|
onerror="this.onerror=null; this.src='/images/placeholder.png'; this.classList.add('bg-light');">
|
|
<% } 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>
|
|
<%= item.name %>
|
|
</strong>
|
|
<% if (item.link) { %>
|
|
<br><small class="text-muted">
|
|
<%= item.link %>
|
|
</small>
|
|
<% } %>
|
|
</td>
|
|
<td>
|
|
<span class="fw-bold text-primary">$<%= item.price %></span>
|
|
<% if (item.priceText) { %>
|
|
<br><small class="text-muted">
|
|
<%= item.priceText %>
|
|
</small>
|
|
<% } %>
|
|
</td>
|
|
<td>
|
|
<% if (item.season && item.season.length> 0) { %>
|
|
<% item.season.forEach(s => { %>
|
|
<span
|
|
class="badge bg-<%= s === 'summer' ? 'warning' : s === 'spring' ? 'success' : s === 'autumn' ? 'danger' : 'info' %> text-dark">
|
|
<%= s %>
|
|
</span>
|
|
<% }) %>
|
|
<% } else { %>
|
|
<span class="text-muted">-</span>
|
|
<% } %>
|
|
</td>
|
|
<td>
|
|
<% if (item.locations && item.locations.length> 0) { %>
|
|
<%= item.locations.join(', ') %>
|
|
<% } else { %>
|
|
<span class="text-muted">-</span>
|
|
<% } %>
|
|
</td>
|
|
<td>
|
|
<% for (let i = 0; i < 5; i++) { %>
|
|
<i class="fas fa-star <%= i < item.rating ? ' text-warning' : 'text-muted' %>"
|
|
style="font-size: 0.8rem;"></i>
|
|
<% } %>
|
|
</td>
|
|
|
|
<!-- Bookings Column -->
|
|
<td>
|
|
<% if (item.bookingCount > 0) { %>
|
|
<span class="badge bg-primary" title="Total bookings">
|
|
<i class="fas fa-users me-1"></i><%= item.bookingCount %>
|
|
</span>
|
|
<% if (item.bookingSessions && item.bookingSessions.length > 0) { %>
|
|
<button type="button" class="btn btn-link btn-sm p-0 ms-1 session-booking-toggle"
|
|
data-bs-toggle="popover"
|
|
data-bs-trigger="click"
|
|
data-bs-html="true"
|
|
data-bs-title="Bookings by Session"
|
|
data-bs-content="<%
|
|
let popoverContent = '<div class=\'small\'>';
|
|
item.bookingSessions.forEach(function(session) {
|
|
const startDate = new Date(session.startDate).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
|
|
const endDate = new Date(session.endDate).toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'2-digit'});
|
|
const sessionBookings = (item.sessionBookingCounts && item.sessionBookingCounts[session.sessionId]) || 0;
|
|
const totalSpots = (session.totalMaleSpots || 0) + (session.totalFemaleSpots || 0);
|
|
const statusClass = sessionBookings >= totalSpots ? 'text-danger' : sessionBookings > totalSpots * 0.8 ? 'text-warning' : 'text-success';
|
|
popoverContent += '<div class=\'mb-1\'><strong>' + startDate + ' - ' + endDate + '</strong>: <span class=\'' + statusClass + '\'>' + sessionBookings + '/' + totalSpots + '</span></div>';
|
|
});
|
|
popoverContent += '</div>';
|
|
%><%= popoverContent %>"
|
|
title="View session details">
|
|
<i class="fas fa-info-circle text-info"></i>
|
|
</button>
|
|
<% } %>
|
|
<% } else { %>
|
|
<span class="text-muted">0</span>
|
|
<% } %>
|
|
</td>
|
|
|
|
<td>
|
|
<div class="form-check form-switch">
|
|
<input type="checkbox" class="form-check-input status-toggle"
|
|
data-id="<%= item._id %>" <%= item.isActive ? 'checked' : '' %>>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="/admin/activity/<%= item._id %>/edit"
|
|
class="btn btn-outline-primary" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
<button type="button" class="btn btn-outline-info preview-btn"
|
|
data-id="<%= item._id %>" title="Preview">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger delete-btn"
|
|
data-id="<%= item._id %>" data-name="<%= item.name %>"
|
|
title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<% }) %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<% if (pagination.totalPages> 1) { %>
|
|
<div class="card-footer bg-white border-top">
|
|
<nav aria-label="Activities pagination">
|
|
<ul class="pagination pagination-sm mb-0 justify-content-center">
|
|
<li class="page-item <%= pagination.hasPrev ? '' : 'disabled' %>">
|
|
<a class="page-link"
|
|
href="?page=<%= pagination.page - 1 %>&limit=<%= pagination.limit %>">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</a>
|
|
</li>
|
|
<% for (let i = 1; i <= pagination.totalPages; i++) { %>
|
|
<% if (i === 1 || i === pagination.totalPages || (i >= pagination.page - 2 && i
|
|
<= pagination.page + 2)) { %>
|
|
<li class="page-item <%= i === pagination.page ? 'active' : '' %>">
|
|
<a class="page-link"
|
|
href="?page=<%= i %>&limit=<%= pagination.limit %>">
|
|
<%= i %>
|
|
</a>
|
|
</li>
|
|
<% } else if (i === pagination.page - 3 || i === pagination.page + 3) {
|
|
%>
|
|
<li class="page-item disabled"><span
|
|
class="page-link">...</span></li>
|
|
<% } %>
|
|
<% } %>
|
|
<li
|
|
class="page-item <%= pagination.hasNext ? '' : 'disabled' %>">
|
|
<a class="page-link"
|
|
href="?page=<%= pagination.page + 1 %>&limit=<%= pagination.limit %>">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
<% } %>
|
|
|
|
<% } else { %>
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-running fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No activities found</h5>
|
|
<p class="text-muted mb-4">Get started by creating your first activity</p>
|
|
<a href="/admin/activity/create" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add First Activity
|
|
</a>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters Tab Pane -->
|
|
<div class="tab-pane fade" id="filters-tab-pane" role="tabpanel" aria-labelledby="filters-tab" tabindex="0">
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="fas fa-filter me-2"></i>Filter Settings</h5>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="addFilterBtn">
|
|
<i class="fas fa-plus me-1"></i>Add Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<form id="filtersForm" method="POST" action="/admin/activity/filters/update" class="content-with-fixed-buttons">
|
|
<div id="filtersContainer">
|
|
<% if (filters && filters.length > 0) { %>
|
|
<% filters.forEach((f, idx) => { %>
|
|
<div class="card mb-3 filter-card" data-filter-index="<%= idx %>">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">Filter: <%= f.label || 'Untitled' %></h6>
|
|
<button type="button" class="btn btn-sm btn-outline-danger delete-filter-btn">
|
|
<i class="fas fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="hidden" name="filters[<%= idx %>][id]" value="<%= f._id %>">
|
|
|
|
<!-- Filter Basic Info -->
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Label</label>
|
|
<input class="form-control filter-label" name="filters[<%= idx %>][label]" value="<%= f.label %>" placeholder="Filter Display Name">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Value (unique key)</label>
|
|
<input class="form-control filter-value" name="filters[<%= idx %>][value]" value="<%= f.value %>" placeholder="unique-key">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Order</label>
|
|
<input type="number" class="form-control" name="filters[<%= idx %>][order]" value="<%= f.order || (idx + 1) %>" placeholder="1">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Items -->
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label mb-0">Filter Options</label>
|
|
<button type="button" class="btn btn-sm btn-outline-primary add-item-btn">
|
|
<i class="fas fa-plus"></i> Add Option
|
|
</button>
|
|
</div>
|
|
<!-- Header row for item columns: Value | Label -->
|
|
<div class="row g-2 mb-2 text-muted small fw-semibold filter-items-headers" style="border-bottom:1px solid #eef2f5; padding-bottom:6px;">
|
|
<div class="col-md-5">Value</div>
|
|
<div class="col-md-6">Label</div>
|
|
<div class="col-md-1" aria-hidden="true"></div>
|
|
</div>
|
|
|
|
<div class="items-container">
|
|
<% (f.items || []).forEach((item, itemIdx) => { %>
|
|
<div class="row g-2 mb-2 filter-item">
|
|
<div class="col-md-5">
|
|
<input class="form-control item-value" placeholder="Value" value="<%= item.value %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<input class="form-control item-label" placeholder="Display Label" value="<%= item.label %>">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<button type="button" class="btn btn-sm btn-outline-danger remove-item-btn">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }) %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden JSON field for items (populated by JS) -->
|
|
<textarea class="d-none filter-items-json" name="filters[<%= idx %>][items]"><%= JSON.stringify(f.items || []) %></textarea>
|
|
</div>
|
|
</div>
|
|
<% }) %>
|
|
<% } %>
|
|
</div>
|
|
|
|
<% if (!filters || filters.length === 0) { %>
|
|
<div class="alert alert-info" id="noFiltersAlert">
|
|
<i class="fas fa-info-circle me-2"></i>No filters configured yet.
|
|
</div>
|
|
<% } %>
|
|
|
|
<!-- Fixed Bottom Save Button -->
|
|
<div class="fixed-bottom-buttons">
|
|
<button type="submit" class="btn btn-primary" id="saveFiltersBtn">
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- All Bookings Tab Pane -->
|
|
<div class="tab-pane fade" id="bookings-tab-pane" role="tabpanel" aria-labelledby="bookings-tab" tabindex="0">
|
|
<!-- Booking Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm bg-primary text-white">
|
|
<div class="card-body text-center py-3">
|
|
<i class="fas fa-clipboard-list fa-2x mb-2"></i>
|
|
<h4 class="mb-0"><%= allBookingsStats ? allBookingsStats.total : 0 %></h4>
|
|
<small>Total Bookings</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm bg-success text-white">
|
|
<div class="card-body text-center py-3">
|
|
<i class="fas fa-check-circle fa-2x mb-2"></i>
|
|
<h4 class="mb-0"><%= allBookingsStats ? allBookingsStats.confirmed : 0 %></h4>
|
|
<small>Confirmed</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm bg-warning text-white">
|
|
<div class="card-body text-center py-3">
|
|
<i class="fas fa-clock fa-2x mb-2"></i>
|
|
<h4 class="mb-0"><%= allBookingsStats ? allBookingsStats.pending : 0 %></h4>
|
|
<small>Pending</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm bg-danger text-white">
|
|
<div class="card-body text-center py-3">
|
|
<i class="fas fa-times-circle fa-2x mb-2"></i>
|
|
<h4 class="mb-0"><%= allBookingsStats ? allBookingsStats.cancelled : 0 %></h4>
|
|
<small>Cancelled</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm bg-info text-white">
|
|
<div class="card-body text-center py-3">
|
|
<i class="fas fa-flag-checkered fa-2x mb-2"></i>
|
|
<h4 class="mb-0"><%= allBookingsStats ? allBookingsStats.completed : 0 %></h4>
|
|
<small>Completed</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card border-0 shadow-sm bg-dark text-white">
|
|
<div class="card-body text-center py-3">
|
|
<i class="fas fa-dollar-sign fa-2x mb-2"></i>
|
|
<h4 class="mb-0">$<%= allBookingsStats ? allBookingsStats.totalRevenue.toLocaleString() : 0 %></h4>
|
|
<small>Revenue</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bookings Table -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="fas fa-calendar-check me-2"></i>All Booking Submissions</h5>
|
|
<div class="d-flex gap-2">
|
|
<input type="text" class="form-control form-control-sm" id="bookingSearchInput"
|
|
placeholder="Search bookings..." style="width: 200px;">
|
|
<select class="form-select form-select-sm" id="bookingStatusFilter" style="width: 150px;">
|
|
<option value="">All Status</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="confirmed">Confirmed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
<option value="completed">Completed</option>
|
|
</select>
|
|
<button type="button" class="btn btn-outline-success btn-sm" id="exportAllBookingsBtn">
|
|
<i class="fas fa-download me-1"></i>Export CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<% if (allBookings && allBookings.length > 0) { %>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm mb-0" id="allBookingsTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width: 40px;">#</th>
|
|
<th>Date</th>
|
|
<th>Activity</th>
|
|
<th>Session</th>
|
|
<th>Participant</th>
|
|
<th>Parent/Guardian</th>
|
|
<th>Contact</th>
|
|
<th>Gender/Age</th>
|
|
<th>Status</th>
|
|
<th>Payment</th>
|
|
<th style="width: 100px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="allBookingsBody">
|
|
<% allBookings.forEach((booking, index) => { %>
|
|
<tr class="booking-row"
|
|
data-status="<%= booking.status %>"
|
|
data-activity="<%= booking.activityId ? booking.activityId.name : '' %>"
|
|
data-participant="<%= booking.participantFirstName %> <%= booking.participantLastName %>"
|
|
data-parent="<%= booking.parentFirstName %> <%= booking.parentLastName %>"
|
|
data-email="<%= booking.email %>">
|
|
<td class="text-muted"><%= index + 1 %></td>
|
|
<td>
|
|
<small><%= new Date(booking.createdAt).toLocaleDateString('en-GB', {day:'2-digit', month:'short', year:'2-digit'}) %></small>
|
|
</td>
|
|
<td>
|
|
<% if (booking.activityId) { %>
|
|
<a href="/admin/activity/<%= booking.activityId._id %>/edit" class="text-decoration-none">
|
|
<strong><%= booking.activityId.name %></strong>
|
|
</a>
|
|
<% } else { %>
|
|
<span class="text-muted">Unknown</span>
|
|
<% } %>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted" title="<%= booking.sessionId %>">
|
|
<%= booking.sessionId ? booking.sessionId.substring(0, 15) + '...' : '-' %>
|
|
</small>
|
|
</td>
|
|
<td>
|
|
<strong><%= booking.participantFirstName %> <%= booking.participantLastName %></strong>
|
|
<small class="d-block text-muted">
|
|
DOB: <%= new Date(booking.participantBirthDate).toLocaleDateString('en-GB') %>
|
|
</small>
|
|
</td>
|
|
<td>
|
|
<strong><%= booking.parentFirstName %> <%= booking.parentLastName %></strong>
|
|
</td>
|
|
<td>
|
|
<small><%= booking.email %></small>
|
|
<small class="d-block text-muted"><%= booking.phone %></small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-<%= booking.participantGender === 'male' ? 'primary' : booking.participantGender === 'female' ? 'danger' : 'secondary' %>">
|
|
<%= booking.participantGender %>
|
|
</span>
|
|
<%
|
|
const today = new Date();
|
|
const birth = new Date(booking.participantBirthDate);
|
|
let age = today.getFullYear() - birth.getFullYear();
|
|
const monthDiff = today.getMonth() - birth.getMonth();
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) age--;
|
|
%>
|
|
<small class="d-block"><%= age %>y</small>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-<%= booking.status === 'confirmed' ? 'success' : booking.status === 'pending' ? 'warning' : booking.status === 'cancelled' ? 'danger' : 'info' %>">
|
|
<%= booking.status %>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-<%= booking.paymentStatus === 'paid' ? 'success' : booking.paymentStatus === 'partial' ? 'info' : booking.paymentStatus === 'refunded' ? 'secondary' : 'warning' %>">
|
|
<%= booking.paymentStatus %>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<% if (booking.activityId) { %>
|
|
<a href="/admin/activity/<%= booking.activityId._id %>/edit?scrollTo=<%= booking.sessionId %>#bookings"
|
|
class="btn btn-outline-primary btn-sm" title="View in Activity">
|
|
<i class="fas fa-external-link-alt"></i>
|
|
</a>
|
|
<% } %>
|
|
<button type="button" class="btn btn-outline-info btn-sm view-booking-btn"
|
|
data-booking='<%= JSON.stringify(booking) %>' title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<% }) %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% } else { %>
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No bookings yet</h5>
|
|
<p class="text-muted mb-0">Booking submissions will appear here when customers book activities.</p>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Booking Details Modal (kept for fallback) -->
|
|
<div class="modal fade" id="bookingViewModal" tabindex="-1" aria-labelledby="bookingViewModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-info text-white">
|
|
<h5 class="modal-title" id="bookingViewModalLabel"><i class="fas fa-user-circle me-2"></i>Booking Details</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="bookingViewContent">
|
|
<!-- Content will be populated by JS -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Booking Details Overlay -->
|
|
<div id="bookingDetailsOverlay" role="dialog" aria-hidden="true">
|
|
<div class="overlay-panel" id="bookingOverlayPanel">
|
|
<div class="overlay-header">
|
|
<div><i class="fas fa-user-circle me-2"></i><strong>Booking Details</strong></div>
|
|
<div>
|
|
<button type="button" class="btn btn-sm btn-light" id="bookingOverlayCloseBtn" title="Close">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="overlay-body" id="bookingOverlayContent">
|
|
<!-- content populated by JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" 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">
|
|
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete "<span id="deleteItemName"></span>"?</p>
|
|
<p class="text-danger"><small>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="deleteForm" method="POST" style="display: inline;">
|
|
<button type="submit" class="btn btn-danger" id="confirmDeleteBtn">
|
|
<i class="fas fa-trash me-1"></i>Delete
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Modal -->
|
|
<div class="modal fade" id="previewModal" tabindex="-1" arialabelledby="previewModalLabel" aria-hidden="true"
|
|
data-bs-backdrop="true" data-bs-keyboard="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="previewModalLabel">Activity Preview</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" id="previewContent">
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Fix modal z-index - must be higher than everything else */
|
|
.modal {
|
|
z-index: 2050 !important;
|
|
}
|
|
|
|
.modal-backdrop {
|
|
z-index: 2040 !important;
|
|
}
|
|
|
|
.modal-dialog {
|
|
z-index: 2060 !important;
|
|
}
|
|
|
|
/* Ensure modal content is visible */
|
|
.modal-content {
|
|
position: relative;
|
|
z-index: 2070 !important;
|
|
background: #fff;
|
|
box-shadow: 0 10px 50px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
/* Force modal to be fixed positioned above everything */
|
|
#deleteModal,
|
|
#previewModal {
|
|
position: fixed !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
overflow-x: hidden !important;
|
|
overflow-y: auto !important;
|
|
background-color: rgba(0, 0, 0, 0) !important;
|
|
transition: background-color 0.15s linear !important;
|
|
}
|
|
|
|
/* Add dark overlay when modal is shown */
|
|
#deleteModal.show,
|
|
#previewModal.show {
|
|
background-color: rgba(0, 0, 0, 0.5) !important;
|
|
}
|
|
|
|
/* Center the modal dialog */
|
|
#deleteModal .modal-dialog,
|
|
#previewModal .modal-dialog {
|
|
display: flex;
|
|
align-items: center;
|
|
min-height: calc(100% - 1rem);
|
|
margin: 0.5rem auto;
|
|
}
|
|
|
|
/* Custom Booking Details Overlay */
|
|
#bookingDetailsOverlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(0,0,0,0.45);
|
|
z-index: 3000;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
#bookingDetailsOverlay.show {
|
|
display: flex;
|
|
}
|
|
|
|
#bookingDetailsOverlay .overlay-panel {
|
|
width: 100%;
|
|
max-width: 1000px;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
box-shadow: 0 10px 60px rgba(0,0,0,0.5);
|
|
position: relative;
|
|
}
|
|
|
|
#bookingDetailsOverlay .overlay-header {
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #eee;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: linear-gradient(90deg,#0dcaf0,#198754);
|
|
color: #fff;
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
}
|
|
|
|
#bookingDetailsOverlay .overlay-body {
|
|
padding: 1rem;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Move modals to body to ensure they are on top of everything
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
const previewModal = document.getElementById('previewModal');
|
|
|
|
if (deleteModal && deleteModal.parentElement !== document.body) {
|
|
document.body.appendChild(deleteModal);
|
|
}
|
|
if (previewModal && previewModal.parentElement !== document.body) {
|
|
document.body.appendChild(previewModal);
|
|
}
|
|
|
|
// Search functionality
|
|
const searchInput = document.getElementById('searchInput');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function () {
|
|
const searchTerm = this.value.toLowerCase();
|
|
const rows = document.querySelectorAll('#activitiesBody tr');
|
|
|
|
rows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
// Toggle status
|
|
document.querySelectorAll('.status-toggle').forEach(toggle => {
|
|
toggle.addEventListener('change', async function () {
|
|
const id = this.dataset.id;
|
|
const checkbox = this;
|
|
|
|
try {
|
|
const response = await fetch(`/admin/activity/${id}/toggle-status`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
showToast('Success', result.message, 'success');
|
|
// If server returned global counts, use them; otherwise update from DOM
|
|
if (typeof result.activeCount !== 'undefined' && typeof result.total !== 'undefined') {
|
|
document.getElementById('activeCount').textContent = result.activeCount;
|
|
document.getElementById('inactiveCount').textContent = result.total - result.activeCount;
|
|
} else if (typeof result.activeCount !== 'undefined') {
|
|
document.getElementById('activeCount').textContent = result.activeCount;
|
|
// try to adjust inactive based on totalCount element if available
|
|
const totalEl = document.getElementById('totalCount');
|
|
if (totalEl) {
|
|
const totalPossible = parseInt(totalEl.textContent || '0', 10) || 0;
|
|
document.getElementById('inactiveCount').textContent = totalPossible - result.activeCount;
|
|
}
|
|
} else {
|
|
updateCounts();
|
|
}
|
|
} else {
|
|
checkbox.checked = !checkbox.checked;
|
|
showToast('Error', result.error || 'Failed to update status', 'error');
|
|
}
|
|
} catch (error) {
|
|
checkbox.checked = !checkbox.checked;
|
|
console.error('Toggle status error:', error);
|
|
showToast('Error', 'Network error: Failed to update status', 'error');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Delete button
|
|
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', function () {
|
|
const id = this.dataset.id;
|
|
const name = this.dataset.name;
|
|
|
|
document.getElementById('deleteItemName').textContent = name;
|
|
document.getElementById('deleteForm').action = `/admin/activity/${id}/delete`;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
|
modal.show();
|
|
});
|
|
});
|
|
|
|
// Handle delete form submission with loading state
|
|
const deleteForm = document.getElementById('deleteForm');
|
|
if (deleteForm) {
|
|
deleteForm.addEventListener('submit', function(e) {
|
|
const deleteBtn = document.getElementById('confirmDeleteBtn');
|
|
if (deleteBtn) {
|
|
deleteBtn.disabled = true;
|
|
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Deleting...';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Preview button
|
|
document.querySelectorAll('.preview-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function () {
|
|
const id = this.dataset.id;
|
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
const content = document.getElementById('previewContent');
|
|
|
|
content.innerHTML = `
|
|
<div class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
modal.show();
|
|
|
|
try {
|
|
const response = await fetch(`/admin/activity/${id}/preview`);
|
|
const data = await response.json();
|
|
|
|
// Handle image path - data should already have full URLs from server
|
|
let imgSrc = data.image || '';
|
|
// If still relative, construct full URL
|
|
if (imgSrc && !imgSrc.startsWith('http')) {
|
|
if (!imgSrc.startsWith('/')) {
|
|
imgSrc = '/uploads/' + imgSrc;
|
|
}
|
|
imgSrc = window.location.origin + imgSrc;
|
|
}
|
|
|
|
content.innerHTML = `
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
${imgSrc ? `<img src="${imgSrc}" class="img-fluid rounded" alt="${data.name}" onerror="this.onerror=null; this.parentElement.innerHTML='<div class=\\'bg-light rounded p-5 text-center\\'><i class=\\'fas fa-image fa-3x text-muted\\'></i></div>';">` :
|
|
'<div class="bg-light rounded p-5 text-center"><i class="fas fa-image fa-3x text-muted"></i></div>'}
|
|
</div>
|
|
<div class="col-md-7">
|
|
<h4>${data.name}</h4>
|
|
<p class="text-primary fs-4 fw-bold">$${data.price}</p>
|
|
<p class="text-muted">${data.priceText || ''}</p>
|
|
|
|
<div class="mb-3">
|
|
<strong>Season:</strong>
|
|
${(data.season || []).map(s => `<span class="badge bg-secondary">${s}</span>`).join(' ')}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Age Range:</strong> ${data.age ? data.age.join(' - ') : 'N/A'} years
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Locations:</strong> ${(data.locations || []).join(', ') || 'N/A'}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Rating:</strong>
|
|
${'<i class="fas fa-star text-warning"></i>'.repeat(data.rating || 0)}
|
|
${'<i class="fas fa-star text-muted"></i>'.repeat(5 - (data.rating || 0))}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Program:</strong> ${data.program || 'N/A'}
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<strong>Link:</strong> <code>${data.link || 'N/A'}</code>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>Status:</strong>
|
|
<span class="badge ${data.isActive ? 'bg-success' : 'bg-secondary'}">
|
|
${data.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (error) {
|
|
console.error('Preview fetch error:', error);
|
|
content.innerHTML = `
|
|
<div class="text-center py-4 text-danger">
|
|
<i class="fas fa-exclamation-circle fa-3x mb-3"></i>
|
|
<p>Failed to load preview</p>
|
|
<small class="text-muted">${error.message || 'Network error'}</small>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
function updateCounts() {
|
|
const activeCount = document.querySelectorAll('.status-toggle:checked').length;
|
|
const totalRows = document.querySelectorAll('.status-toggle').length;
|
|
|
|
document.getElementById('activeCount').textContent = activeCount;
|
|
document.getElementById('inactiveCount').textContent = totalRows - activeCount;
|
|
}
|
|
|
|
// Filter management functionality
|
|
let filterIndex = <%= filters ? filters.length : 0 %>;
|
|
|
|
// Add new filter
|
|
document.getElementById('addFilterBtn').addEventListener('click', function() {
|
|
const container = document.getElementById('filtersContainer');
|
|
const noFiltersAlert = document.getElementById('noFiltersAlert');
|
|
|
|
if (noFiltersAlert) {
|
|
noFiltersAlert.style.display = 'none';
|
|
}
|
|
|
|
const filterHtml = `
|
|
<div class="card mb-3 filter-card" data-filter-index="${filterIndex}">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">Filter: New Filter</h6>
|
|
<button type="button" class="btn btn-sm btn-outline-danger delete-filter-btn">
|
|
<i class="fas fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="hidden" name="filters[${filterIndex}][id]" value="">
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Label</label>
|
|
<input class="form-control filter-label" name="filters[${filterIndex}][label]" value="" placeholder="Filter Display Name">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Value (unique key)</label>
|
|
<input class="form-control filter-value" name="filters[${filterIndex}][value]" value="" placeholder="unique-key">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Order</label>
|
|
<input type="number" class="form-control" name="filters[${filterIndex}][order]" value="${filterIndex + 1}" placeholder="1">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label mb-0">Filter Options</label>
|
|
<button type="button" class="btn btn-sm btn-outline-primary add-item-btn">
|
|
<i class="fas fa-plus"></i> Add Option
|
|
</button>
|
|
</div>
|
|
<!-- Header row for item columns: Value | Label -->
|
|
<div class="row g-2 mb-2 text-muted small fw-semibold filter-items-headers" style="border-bottom:1px solid #eef2f5; padding-bottom:6px;">
|
|
<div class="col-md-5">Value</div>
|
|
<div class="col-md-6">Label</div>
|
|
<div class="col-md-1" aria-hidden="true"></div>
|
|
</div>
|
|
<div class="items-container"></div>
|
|
</div>
|
|
|
|
<textarea class="d-none filter-items-json" name="filters[${filterIndex}][items]">[]</textarea>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.insertAdjacentHTML('beforeend', filterHtml);
|
|
attachFilterEvents(container.lastElementChild);
|
|
filterIndex++;
|
|
});
|
|
|
|
// Delete filter
|
|
function attachFilterEvents(filterCard) {
|
|
// Delete filter button
|
|
filterCard.querySelector('.delete-filter-btn').addEventListener('click', function() {
|
|
if (confirm('Are you sure you want to delete this filter?')) {
|
|
filterCard.remove();
|
|
updateFilterIndexes();
|
|
}
|
|
});
|
|
|
|
// Update filter title when label changes
|
|
filterCard.querySelector('.filter-label').addEventListener('input', function() {
|
|
const title = filterCard.querySelector('.card-header h6');
|
|
title.textContent = 'Filter: ' + (this.value || 'Untitled');
|
|
});
|
|
|
|
// Add item button
|
|
filterCard.querySelector('.add-item-btn').addEventListener('click', function() {
|
|
addFilterItem(filterCard);
|
|
});
|
|
|
|
// Attach events to existing items
|
|
filterCard.querySelectorAll('.remove-item-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
btn.closest('.filter-item').remove();
|
|
updateFilterItemsJson(filterCard);
|
|
});
|
|
});
|
|
|
|
// Update JSON when items change (value & label only)
|
|
filterCard.querySelectorAll('.item-value, .item-label').forEach(input => {
|
|
input.addEventListener('input', function() {
|
|
updateFilterItemsJson(filterCard);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Add filter item
|
|
function addFilterItem(filterCard) {
|
|
const itemsContainer = filterCard.querySelector('.items-container');
|
|
const itemHtml = `
|
|
<div class="row g-2 mb-2 filter-item">
|
|
<div class="col-md-5">
|
|
<input class="form-control item-value" placeholder="Value" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<input class="form-control item-label" placeholder="Display Label" value="">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<button type="button" class="btn btn-sm btn-outline-danger remove-item-btn">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
itemsContainer.insertAdjacentHTML('beforeend', itemHtml);
|
|
|
|
// Attach events to new item
|
|
const newItem = itemsContainer.lastElementChild;
|
|
newItem.querySelector('.remove-item-btn').addEventListener('click', function() {
|
|
newItem.remove();
|
|
updateFilterItemsJson(filterCard);
|
|
});
|
|
|
|
|
|
newItem.querySelectorAll('.item-value, .item-label').forEach(input => {
|
|
input.addEventListener('input', function() {
|
|
updateFilterItemsJson(filterCard);
|
|
});
|
|
});
|
|
|
|
updateFilterItemsJson(filterCard);
|
|
}
|
|
|
|
// Update JSON representation of items
|
|
function updateFilterItemsJson(filterCard) {
|
|
const items = [];
|
|
filterCard.querySelectorAll('.filter-item').forEach(item => {
|
|
const value = item.querySelector('.item-value').value.trim();
|
|
const label = item.querySelector('.item-label').value.trim();
|
|
|
|
if (value && label) {
|
|
items.push({ value, label });
|
|
}
|
|
});
|
|
|
|
filterCard.querySelector('.filter-items-json').value = JSON.stringify(items);
|
|
}
|
|
|
|
// Update filter indexes after deletion
|
|
function updateFilterIndexes() {
|
|
const filterCards = document.querySelectorAll('.filter-card');
|
|
filterCards.forEach((card, index) => {
|
|
card.dataset.filterIndex = index;
|
|
|
|
// Update all name attributes
|
|
card.querySelectorAll('input[name*="filters["], textarea[name*="filters["]').forEach(input => {
|
|
const name = input.getAttribute('name');
|
|
const newName = name.replace(/filters\[\d+\]/, `filters[${index}]`);
|
|
input.setAttribute('name', newName);
|
|
});
|
|
});
|
|
filterIndex = filterCards.length;
|
|
}
|
|
|
|
// Save filters
|
|
document.getElementById('saveFiltersBtn').addEventListener('click', function() {
|
|
// Update all JSON fields before submitting
|
|
document.querySelectorAll('.filter-card').forEach(filterCard => {
|
|
updateFilterItemsJson(filterCard);
|
|
});
|
|
|
|
// Validate filters
|
|
let isValid = true;
|
|
const values = new Set();
|
|
|
|
document.querySelectorAll('.filter-card').forEach(card => {
|
|
const label = card.querySelector('.filter-label').value.trim();
|
|
const value = card.querySelector('.filter-value').value.trim();
|
|
|
|
if (!label || !value) {
|
|
isValid = false;
|
|
showToast('Validation Error', 'All filters must have a label and value', 'error');
|
|
return;
|
|
}
|
|
|
|
if (values.has(value)) {
|
|
isValid = false;
|
|
showToast('Validation Error', `Duplicate filter value: ${value}`, 'error');
|
|
return;
|
|
}
|
|
|
|
values.add(value);
|
|
});
|
|
|
|
if (isValid) {
|
|
document.getElementById('filtersForm').submit();
|
|
}
|
|
});
|
|
|
|
// Attach events to existing filters
|
|
document.querySelectorAll('.filter-card').forEach(filterCard => {
|
|
attachFilterEvents(filterCard);
|
|
});
|
|
|
|
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.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>
|
|
`;
|
|
|
|
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, { delay: 3000 });
|
|
bsToast.show();
|
|
|
|
toast.addEventListener('hidden.bs.toast', () => toast.remove());
|
|
}
|
|
|
|
// ==================== ALL BOOKINGS TAB ====================
|
|
|
|
// Initialize Bootstrap popovers for session booking details
|
|
document.querySelectorAll('.session-booking-toggle').forEach(el => {
|
|
new bootstrap.Popover(el, {
|
|
container: 'body',
|
|
placement: 'left'
|
|
});
|
|
});
|
|
|
|
// Bookings search functionality
|
|
const bookingSearchInput = document.getElementById('bookingSearchInput');
|
|
const bookingStatusFilter = document.getElementById('bookingStatusFilter');
|
|
|
|
function filterBookingsTable() {
|
|
const searchTerm = (bookingSearchInput ? bookingSearchInput.value : '').toLowerCase();
|
|
const statusFilter = bookingStatusFilter ? bookingStatusFilter.value : '';
|
|
const rows = document.querySelectorAll('#allBookingsBody tr.booking-row');
|
|
|
|
rows.forEach(row => {
|
|
const activity = (row.dataset.activity || '').toLowerCase();
|
|
const participant = (row.dataset.participant || '').toLowerCase();
|
|
const parent = (row.dataset.parent || '').toLowerCase();
|
|
const email = (row.dataset.email || '').toLowerCase();
|
|
const status = row.dataset.status || '';
|
|
|
|
const matchesSearch = !searchTerm ||
|
|
activity.includes(searchTerm) ||
|
|
participant.includes(searchTerm) ||
|
|
parent.includes(searchTerm) ||
|
|
email.includes(searchTerm);
|
|
|
|
const matchesStatus = !statusFilter || status === statusFilter;
|
|
|
|
row.style.display = (matchesSearch && matchesStatus) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
if (bookingSearchInput) {
|
|
bookingSearchInput.addEventListener('input', filterBookingsTable);
|
|
}
|
|
if (bookingStatusFilter) {
|
|
bookingStatusFilter.addEventListener('change', filterBookingsTable);
|
|
}
|
|
|
|
// View booking details
|
|
document.querySelectorAll('.view-booking-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const booking = JSON.parse(this.dataset.booking);
|
|
showBookingDetails(booking);
|
|
});
|
|
});
|
|
|
|
function showBookingDetails(booking) {
|
|
const formatDate = (date) => {
|
|
if (!date) return 'Not specified';
|
|
return new Date(date).toLocaleDateString('en-GB', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
};
|
|
|
|
const calculateAge = (birthDate) => {
|
|
if (!birthDate) return 'Unknown';
|
|
const today = new Date();
|
|
const birth = new Date(birthDate);
|
|
let age = today.getFullYear() - birth.getFullYear();
|
|
const monthDiff = today.getMonth() - birth.getMonth();
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) age--;
|
|
return age + ' years old';
|
|
};
|
|
|
|
const getStatusBadge = (status) => {
|
|
const badges = {
|
|
'pending': '<span class="badge bg-warning">Pending</span>',
|
|
'confirmed': '<span class="badge bg-success">Confirmed</span>',
|
|
'cancelled': '<span class="badge bg-danger">Cancelled</span>',
|
|
'completed': '<span class="badge bg-info">Completed</span>'
|
|
};
|
|
return badges[status] || '<span class="badge bg-secondary">Unknown</span>';
|
|
};
|
|
|
|
const getPaymentBadge = (paymentStatus) => {
|
|
const badges = {
|
|
'pending': '<span class="badge bg-warning">Pending</span>',
|
|
'partial': '<span class="badge bg-info">Partial</span>',
|
|
'paid': '<span class="badge bg-success">Paid</span>',
|
|
'refunded': '<span class="badge bg-secondary">Refunded</span>'
|
|
};
|
|
return badges[paymentStatus] || '<span class="badge bg-secondary">Unknown</span>';
|
|
};
|
|
|
|
const content = `
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-primary text-white py-2">
|
|
<h6 class="mb-0"><i class="fas fa-child me-2"></i>Participant</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1"><strong class="text-primary fs-5">${booking.participantFirstName} ${booking.participantLastName}</strong></p>
|
|
<p class="mb-1"><small class="text-muted">Birth Date:</small> ${formatDate(booking.participantBirthDate)} (${calculateAge(booking.participantBirthDate)})</p>
|
|
<p class="mb-0"><small class="text-muted">Gender:</small>
|
|
<span class="badge bg-${booking.participantGender === 'male' ? 'primary' : booking.participantGender === 'female' ? 'danger' : 'secondary'}">${booking.participantGender}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-success text-white py-2">
|
|
<h6 class="mb-0"><i class="fas fa-user-tie me-2"></i>Parent/Guardian</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1"><strong>${booking.parentFirstName} ${booking.parentLastName}</strong></p>
|
|
<p class="mb-1"><i class="fas fa-envelope me-1"></i> <a href="mailto:${booking.email}">${booking.email}</a></p>
|
|
<p class="mb-0"><i class="fas fa-phone me-1"></i> <a href="tel:${booking.phone}">${booking.phone}</a></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-warning py-2">
|
|
<h6 class="mb-0"><i class="fas fa-map-marker-alt me-2"></i>Address</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1">${booking.address || 'N/A'}</p>
|
|
<p class="mb-1">${booking.city || ''} ${booking.postalCode || ''}</p>
|
|
<p class="mb-0"><span class="badge bg-secondary">${booking.country || 'N/A'}</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-danger text-white py-2">
|
|
<h6 class="mb-0"><i class="fas fa-phone-alt me-2"></i>Emergency Contact</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1"><strong>${booking.emergencyContact || 'N/A'}</strong></p>
|
|
<p class="mb-0"><i class="fas fa-phone me-1"></i> ${booking.emergencyPhone || 'N/A'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card mb-3">
|
|
<div class="card-header bg-info text-white py-2">
|
|
<h6 class="mb-0"><i class="fas fa-notes-medical me-2"></i>Additional Info</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<p class="mb-1"><small class="text-muted">Medical Conditions:</small></p>
|
|
<p class="mb-0">${booking.medicalConditions || 'None specified'}</p>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<p class="mb-1"><small class="text-muted">Dietary Restrictions:</small></p>
|
|
<p class="mb-0">${booking.dietaryRestrictions || 'None'}</p>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<p class="mb-1"><small class="text-muted">Special Requests:</small></p>
|
|
<p class="mb-0">${booking.specialRequests || 'None'}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header bg-secondary text-white py-2">
|
|
<h6 class="mb-0"><i class="fas fa-info-circle me-2"></i>Booking Status</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1"><small class="text-muted">Status:</small> ${getStatusBadge(booking.status)}</p>
|
|
<p class="mb-1"><small class="text-muted">Payment:</small> ${getPaymentBadge(booking.paymentStatus)}</p>
|
|
<p class="mb-1"><small class="text-muted">Total Amount:</small> <strong class="text-success">$${booking.totalAmount || 0}</strong></p>
|
|
<p class="mb-0"><small class="text-muted">Paid Amount:</small> <strong class="text-info">$${booking.paidAmount || 0}</strong></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header bg-dark text-white py-2">
|
|
<h6 class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Session Info</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1"><small class="text-muted">Session ID:</small></p>
|
|
<p class="mb-1"><code>${booking.sessionId || 'N/A'}</code></p>
|
|
<p class="mb-1"><small class="text-muted">Booked on:</small></p>
|
|
<p class="mb-0">${new Date(booking.createdAt).toLocaleString('en-GB')}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${booking.adminNotes ? `
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<div class="alert alert-warning mb-0">
|
|
<strong><i class="fas fa-sticky-note me-1"></i> Admin Notes:</strong> ${booking.adminNotes}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
// Render into custom overlay instead of bootstrap backdrop modal
|
|
const overlay = document.getElementById('bookingDetailsOverlay');
|
|
const overlayContent = document.getElementById('bookingOverlayContent');
|
|
if (overlay && overlayContent) {
|
|
overlayContent.innerHTML = content;
|
|
// Ensure any inline 'display: none' from previous close is cleared
|
|
overlay.style.display = '';
|
|
overlay.classList.add('show');
|
|
|
|
// Close button - attach listener (remove previous if exists)
|
|
const closeBtn = document.getElementById('bookingOverlayCloseBtn');
|
|
if (closeBtn) {
|
|
if (closeBtn._handler) closeBtn.removeEventListener('click', closeBtn._handler);
|
|
closeBtn._handler = closeBookingOverlay;
|
|
closeBtn.addEventListener('click', closeBtn._handler);
|
|
}
|
|
|
|
// Close when clicking outside panel - manage handler reference
|
|
if (overlay._clickHandler) overlay.removeEventListener('click', overlay._clickHandler);
|
|
overlay._clickHandler = function(e) { if (e.target === overlay) closeBookingOverlay(); };
|
|
overlay.addEventListener('click', overlay._clickHandler);
|
|
|
|
// Close on Esc - ensure single listener
|
|
document.removeEventListener('keydown', bookingOverlayEscHandler);
|
|
document.addEventListener('keydown', bookingOverlayEscHandler);
|
|
} else {
|
|
// Fallback to bootstrap modal if overlay missing
|
|
document.getElementById('bookingViewContent').innerHTML = content;
|
|
const modal = new bootstrap.Modal(document.getElementById('bookingViewModal'), { backdrop: false });
|
|
modal.show();
|
|
}
|
|
}
|
|
|
|
function closeBookingOverlay() {
|
|
const overlay = document.getElementById('bookingDetailsOverlay');
|
|
if (!overlay) return;
|
|
overlay.classList.remove('show');
|
|
overlay.style.display = 'none';
|
|
// remove Esc handler
|
|
document.removeEventListener('keydown', bookingOverlayEscHandler);
|
|
}
|
|
|
|
function bookingOverlayEscHandler(e) {
|
|
if (e.key === 'Escape') closeBookingOverlay();
|
|
}
|
|
|
|
// Export all bookings as CSV
|
|
document.getElementById('exportAllBookingsBtn')?.addEventListener('click', function() {
|
|
window.location.href = '/admin/bookings/export-all';
|
|
});
|
|
</script> |