Files
uldp-degree-mangement-system/views/admin/activity/index.ejs
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

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>