Files
cms.uldp.edu.vn/views/admin/activity/form.ejs
2026-04-10 15:55:15 +07:00

4156 lines
234 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">
<%= isEdit ? 'Update activity details' : 'Create a new activity' %>
</p>
</div>
<div>
<a href="/admin/activity" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to List
</a>
</div>
</div>
<div class="row">
<div class="col-md-8">
<form method="POST"
action="<%= isEdit ? '/admin/activity/' + data._id + '/update' : '/admin/activity/create' %>"
id="activityForm">
<% if (!isEdit) { %>
<!-- Hero Section -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-star me-2"></i>Hero Section</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Hero Title</label>
<input type="text" class="form-control" name="heroTitle"
value="<%= (data.hero && data.hero.title) || '' %>"
placeholder="e.g., Activities">
<small class="text-muted">Title for the activities page header</small>
</div>
<div class="col-12">
<label class="form-label fw-medium">Banner Image</label>
<div class="input-group">
<input type="text" class="form-control" name="heroBannerImage" id="heroBannerImage"
value="<%= (data.hero && data.hero.bannerImage) || '' %>"
placeholder="/templates/yootheme/activities/activity-banner.jpg"
maxlength="255" data-maxlength="255"
data-admin-upload-guidance="Activity page hero-style image.|Recommended upload: at least 1920x700px.">
<button type="button" class="btn btn-outline-secondary" id="uploadHeroBannerBtn">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<div class="mt-2" id="heroBannerPreviewWrapper" style="display: none;">
<img id="heroBannerPreview" src="" alt="Hero banner preview"
style="max-width:100%;height:auto;border:1px solid #ddd;padding:4px;border-radius:4px;">
</div>
</div>
</div>
</div>
</div>
<% } %>
<!-- Basic Information -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Basic Information</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Activity Name <span
class="text-danger">*</span></label>
<input type="text" class="form-control" name="name" value="<%= data.name || '' %>"
required placeholder="e.g., Adventure Sports Camp">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Price (USD) <span
class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" class="form-control" name="price"
value="<%= data.price || 0 %>" required min="0" step="1">
</div>
</div>
<div class="col-md-8">
<label class="form-label fw-medium">Price Text</label>
<input type="text" class="form-control" name="priceText"
value="<%= data.priceText || '' %>" placeholder="e.g., from 395 USD">
<small class="text-muted">Leave empty to auto-generate</small>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Link/URL Slug</label>
<div class="input-group">
<span class="input-group-text">/</span>
<input type="text" class="form-control" name="link"
value="<%= (data.link || '').replace(/^\//, '') %>"
placeholder="e.g., adventure-sports">
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Program Code</label>
<input type="text" class="form-control" name="program" value="<%= data.program || '' %>"
placeholder="e.g., adventure">
</div>
</div>
</div>
</div>
<!-- Season & Age -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Season & Age</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Seasons</label>
<div class="d-flex flex-wrap gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="season" value="spring"
id="seasonSpring" <%=(data.season || []).includes('spring') ? 'checked' : ''
%>>
<label class="form-check-label" for="seasonSpring">
<i class="fas fa-seedling text-success me-1"></i>Spring
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="season" value="summer"
id="seasonSummer" <%=(data.season || []).includes('summer') ? 'checked' : ''
%>>
<label class="form-check-label" for="seasonSummer">
<i class="fas fa-sun text-warning me-1"></i>Summer
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="season" value="autumn"
id="seasonAutumn" <%=(data.season || []).includes('autumn') ? 'checked' : ''
%>>
<label class="form-check-label" for="seasonAutumn">
<i class="fas fa-leaf text-danger me-1"></i>Autumn
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="season" value="winter"
id="seasonWinter" <%=(data.season || []).includes('winter') ? 'checked' : ''
%>>
<label class="form-check-label" for="seasonWinter">
<i class="fas fa-snowflake text-info me-1"></i>Winter
</label>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Age Range</label>
<div class="row g-2">
<div class="col-6">
<div class="input-group">
<span class="input-group-text">Min</span>
<input type="number" class="form-control" name="ageMin"
value="<%= (data.age && data.age[0]) || 12 %>" min="1" max="99">
</div>
</div>
<div class="col-6">
<div class="input-group">
<span class="input-group-text">Max</span>
<input type="number" class="form-control" name="ageMax"
value="<%= (data.age && data.age[1]) || 18 %>" min="1" max="99">
</div>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Rating</label>
<div class="rating-input">
<div class="btn-group" role="group">
<% for (let i=1; i <=5; i++) { %>
<input type="radio" class="btn-check" name="rating" value="<%= i %>"
id="rating<%= i %>" <%=(data.rating || 4)===i ? 'checked' : '' %>>
<label class="btn btn-outline-warning" for="rating<%= i %>">
<%= i %> <i class="fas fa-star"></i>
</label>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Locations -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-map-marker-alt me-2"></i>Locations</h5>
</div>
<div class="card-body">
<label class="form-label fw-medium">Available Locations</label>
<div class="row g-2">
<% const allLocations=['vietnam', 'thailand' , 'philippines' , 'malaysia' , 'china'
, 'portugal' ]; %>
<% allLocations.forEach(loc=> { %>
<div class="col-md-4 col-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="locations"
value="<%= loc %>" id="loc_<%= loc %>" <%=(data.locations ||
[]).includes(loc) ? 'checked' : '' %>>
<label class="form-check-label text-capitalize" for="loc_<%= loc %>">
<i class="fas fa-globe me-1"></i>
<%= loc %>
</label>
</div>
</div>
<% }) %>
</div>
<hr>
<div class="row">
<div class="col-12">
<label class="form-label">Or add custom locations (comma separated)</label>
<input type="text" class="form-control" id="customLocations"
placeholder="e.g., singapore, indonesia">
</div>
</div>
</div>
</div>
<!-- Basic Image -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-image me-2"></i>Featured Image</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Image Path</label>
<input type="text" class="form-control" name="image" id="imageInput"
value="<%= data.image || '' %>"
placeholder="e.g., yootheme/banner/b14.jpg"
maxlength="255" data-maxlength="255"
data-admin-upload-guidance="Used in activity listing cards.|Recommended upload: a landscape image at 704x432px or larger.">
<small class="text-muted">Path to the main activity image (used in listings)</small>
</div>
</div>
</div>
</div>
<!-- Camp Details -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Camp Details</h5>
</div>
<div class="card-body">
<div class="accordion" id="campDetailAccordion">
<!-- Hero Section -->
<div class="accordion-item">
<h2 class="accordion-header" id="heroHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#heroCollapse" aria-expanded="false">
<i class="fas fa-star me-2"></i>Hero Section
</button>
</h2>
<div id="heroCollapse" class="accordion-collapse collapse" data-bs-parent="#campDetailAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Hero Title</label>
<input type="text" class="form-control" name="campDetailHeroTitle"
value="<%= (data.campDetail && data.campDetail.hero && data.campDetail.hero.title) || '' %>"
placeholder="e.g., Adventure Sports Camp in Thailand">
</div>
<div class="col-12">
<label class="form-label fw-medium">Hero Background Image</label>
<div class="input-group">
<input type="text" class="form-control" name="campDetailHeroBgImage" id="campDetailHeroBgImage"
value="<%= (data.campDetail && data.campDetail.hero && data.campDetail.hero.bgImage) || '' %>"
placeholder="e.g., yootheme/banner/b1.jpg"
maxlength="255" data-maxlength="255"
data-admin-upload-guidance="Activity page hero-style image.|Recommended upload: at least 1920x700px.">
<button type="button" class="btn btn-outline-secondary" id="uploadHeroBgBtn"><i class="fas fa-upload me-1"></i>Upload</button>
</div>
<div class="mt-2" id="campDetailHeroBgPreviewWrapper" style="display: none;">
<img id="campDetailHeroBgPreview" src="" alt="Hero background preview" style="max-width:100%;height:auto;border:1px solid #ddd;padding:4px;border-radius:4px;">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Basic Info Section -->
<div class="accordion-item">
<h2 class="accordion-header" id="basicInfoHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#basicInfoCollapse" aria-expanded="false">
<i class="fas fa-info me-2"></i>Basic Information
</button>
</h2>
<div id="basicInfoCollapse" class="accordion-collapse collapse" data-bs-parent="#campDetailAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Location</label>
<input type="text" class="form-control" name="campDetailBasicInfoLocation"
value="<%= (data.campDetail && data.campDetail.basicInfo && data.campDetail.basicInfo.location) || '' %>"
placeholder="e.g., Thailand">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Age Range</label>
<textarea class="form-control" name="campDetailBasicInfoAgeRange" rows="2"
placeholder="e.g., 12 - 18 years&#10;Separated by age groups"><%= (data.campDetail && data.campDetail.basicInfo && data.campDetail.basicInfo.ageRange) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Accommodation Type</label>
<input type="text" class="form-control" name="campDetailBasicInfoAccommodationType"
value="<%= (data.campDetail && data.campDetail.basicInfo && data.campDetail.basicInfo.accommodationType) || '' %>"
placeholder="e.g., Beach Resort & Dive Center">
</div>
<div class="col-12">
<label class="form-label fw-medium">Care Level</label>
<input type="text" class="form-control" name="campDetailBasicInfoCareLevel"
value="<%= (data.campDetail && data.campDetail.basicInfo && data.campDetail.basicInfo.careLevel) || '' %>"
placeholder="e.g., Around-the-Clock Care & All Meals Included">
</div>
<div class="col-12">
<label class="form-label fw-medium">Languages</label>
<textarea class="form-control" name="campDetailBasicInfoLanguages" rows="2"
placeholder="e.g., Bilingual&#10;EN & TH"><%= (data.campDetail && data.campDetail.basicInfo && data.campDetail.basicInfo.languages) || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar Section -->
<div class="accordion-item">
<h2 class="accordion-header" id="sidebarHeader">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#sidebarCollapse" aria-expanded="true">
<i class="fas fa-columns me-2"></i>Sidebar Content
</button>
</h2>
<div id="sidebarCollapse" class="accordion-collapse collapse show" data-bs-parent="#campDetailAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Contact Phone</label>
<input type="text" class="form-control" name="campDetailSidebarContactPhone"
value="<%= (data.campDetail && data.campDetail.sidebar && data.campDetail.sidebar.contact && data.campDetail.sidebar.contact.phone) || '' %>"
placeholder="+(123)-456-789">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Contact Email</label>
<input type="email" class="form-control" name="campDetailSidebarContactEmail"
value="<%= (data.campDetail && data.campDetail.sidebar && data.campDetail.sidebar.contact && data.campDetail.sidebar.contact.email) || '' %>"
placeholder="hello@ggcamp.org">
</div>
<div class="col-12">
<label class="form-label fw-medium">Menu Items</label>
<div id="menuItemsEditor" class="mb-2"></div>
<div class="mb-2">
<button type="button" class="btn btn-sm btn-outline-primary" id="addMenuItemBtn">
<i class="fas fa-plus me-1"></i>Add Menu Item
</button>
</div>
<textarea name="campDetailSidebarMenuItems" id="campDetailSidebarMenuItems" class="form-control d-none" rows="6" placeholder='[{"name": "Overview", "href": "#overview"}, ...]'><%= JSON.stringify(((data.campDetail && data.campDetail.sidebar && data.campDetail.sidebar.menuItems) || (data['camp-detail'] && data['camp-detail'].sidebar && data['camp-detail'].sidebar.menuItems) || []), null, 2) %></textarea>
<small class="text-muted">Use the editor above to add, edit, or remove menu items. The JSON will be saved automatically.</small>
</div>
<div class="col-12">
<label class="form-label fw-medium">Upcoming Tours</label>
<div id="upcomingToursEditor" class="mb-2"></div>
<div class="mb-2">
<button type="button" class="btn btn-sm btn-outline-primary" id="addUpcomingTourBtn">
<i class="fas fa-plus me-1"></i>Add Upcoming Tour
</button>
</div>
<textarea name="campDetailSidebarUpcomingTours" id="campDetailSidebarUpcomingTours" class="form-control d-none" rows="8" placeholder='[{"id": 1, "title": "Tour Name", "rating": 4.9, "reviews": 25, "location": "City", "price": 1500, "originalPrice": 1800, "image": "https://..."}]'><%= JSON.stringify(((data.campDetail && data.campDetail.sidebar && data.campDetail.sidebar.upcomingTours) || (data['camp-detail'] && data['camp-detail'].sidebar && data['camp-detail'].sidebar.upcomingTours) || []), null, 2) %></textarea>
<small class="text-muted">Use the editor above to add, edit, or remove upcoming tours. The JSON will be saved automatically.</small>
</div>
</div>
</div>
</div>
</div>
<!-- Main Gallery Section -->
<div class="accordion-item">
<h2 class="accordion-header" id="mainGalleryHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#mainGalleryCollapse" aria-expanded="false">
<i class="fas fa-images me-2"></i>Main Gallery
</button>
</h2>
<div id="mainGalleryCollapse" class="accordion-collapse collapse" data-bs-parent="#campDetailAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Gallery Slides</label>
<div id="mainGalleryEditor" class="mb-2"></div>
<div class="mb-2">
<button type="button" class="btn btn-sm btn-outline-primary" id="addMainGallerySlideBtn">
<i class="fas fa-plus me-1"></i>Add Slide
</button>
</div>
<textarea id="campDetailMainGallerySlides" name="campDetailMainGallerySlides" class="form-control d-none font-monospace" rows="6"
placeholder='[{"url": "/path/to/image.png", "alt": "Description"}]'><%= JSON.stringify((data.campDetail && data.campDetail.mainGallery && data.campDetail.mainGallery.slides) || [], null, 2) %></textarea>
<small class="text-muted">Use the editor above to manage slides. JSON is saved automatically.</small>
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Overlay Location</label>
<input type="text" class="form-control" name="campDetailMainGalleryOverlayLocation"
value="<%= (data.campDetail && data.campDetail.mainGallery && data.campDetail.mainGallery.overlayInfo && data.campDetail.mainGallery.overlayInfo.location) || '' %>"
placeholder="Thailand">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Overlay Season</label>
<input type="text" class="form-control" name="campDetailMainGalleryOverlaySeason"
value="<%= (data.campDetail && data.campDetail.mainGallery && data.campDetail.mainGallery.overlayInfo && data.campDetail.mainGallery.overlayInfo.season) || '' %>"
placeholder="Spring, Summer, Autumn">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Overlay Languages</label>
<input type="text" class="form-control" name="campDetailMainGalleryOverlayLanguages"
value="<%= (data.campDetail && data.campDetail.mainGallery && data.campDetail.mainGallery.overlayInfo && data.campDetail.mainGallery.overlayInfo.languages) || '' %>"
placeholder="GER & EN">
</div>
</div>
</div>
</div>
</div>
<!-- Event Schedule Section -->
<div class="accordion-item">
<h2 class="accordion-header" id="eventScheduleHeader">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#eventScheduleCollapse" aria-expanded="false">
<i class="fas fa-calendar me-2"></i>Event Schedule
</button>
</h2>
<div id="eventScheduleCollapse" class="accordion-collapse collapse" data-bs-parent="#campDetailAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label fw-medium">Start Date</label>
<input type="text" class="form-control" name="campDetailEventScheduleStartDate"
value="<%= (data.campDetail && data.campDetail.eventSchedule && data.campDetail.eventSchedule.startDate) || '' %>"
placeholder="e.g., 06/20/2024">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Duration</label>
<input type="text" class="form-control" name="campDetailEventScheduleDuration"
value="<%= (data.campDetail && data.campDetail.eventSchedule && data.campDetail.eventSchedule.duration) || '' %>"
placeholder="e.g., 8 Days 7 Nights">
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Tickets</label>
<input type="text" class="form-control" name="campDetailEventScheduleTickets"
value="<%= (data.campDetail && data.campDetail.eventSchedule && data.campDetail.eventSchedule.tickets) || '' %>"
placeholder="e.g., $52/55">
</div>
</div>
</div>
</div>
</div>
<!-- Sections Content -->
<div class="accordion-item">
<h2 class="accordion-header" id="sectionsHeader">
<button class="accordion-button" type="button" data-bs-toggle="collapse"
data-bs-target="#sectionsCollapse" aria-expanded="true">
<i class="fas fa-puzzle-piece me-2"></i>Sections Content
<span class="badge bg-primary ms-2" id="sectionsBadge">7 Sections</span>
</button>
</h2>
<div id="sectionsCollapse" class="accordion-collapse collapse show" data-bs-parent="#campDetailAccordion">
<div class="accordion-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
Manage all camp sections with visual editors. Changes are automatically synchronized to JSON.
</div>
<!-- Sections Navigation Tabs -->
<ul class="nav nav-tabs mb-4" id="sectionsTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview-content" type="button" role="tab">
<i class="fas fa-eye me-1"></i>Overview
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location-content" type="button" role="tab">
<i class="fas fa-map-marker-alt me-1"></i>Location
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="accommodation-tab" data-bs-toggle="tab" data-bs-target="#accommodation-content" type="button" role="tab">
<i class="fas fa-bed me-1"></i>Accommodation
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="program-tab" data-bs-toggle="tab" data-bs-target="#program-content" type="button" role="tab">
<i class="fas fa-calendar-week me-1"></i>Program
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="meals-tab" data-bs-toggle="tab" data-bs-target="#meals-content" type="button" role="tab">
<i class="fas fa-utensils me-1"></i>Meals
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="team-tab" data-bs-toggle="tab" data-bs-target="#team-content" type="button" role="tab">
<i class="fas fa-users me-1"></i>Team
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="insurance-tab" data-bs-toggle="tab" data-bs-target="#insurance-content" type="button" role="tab">
<i class="fas fa-shield-alt me-1"></i>Insurance
</button>
</li>
</ul>
<!-- Tab Contents -->
<div class="tab-content" id="sectionsTabContent">
<!-- Overview Section -->
<div class="tab-pane fade show active" id="overview-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-eye me-2"></i>Overview Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Introduction</label>
<textarea class="form-control" data-section="overview" data-field="intro" rows="3"
placeholder="Brief introduction about the camp experience..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.overview && data.campDetail.sections.overview.intro) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Main Text</label>
<textarea class="form-control" data-section="overview" data-field="mainText" rows="4"
placeholder="Main description of the camp program..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.overview && data.campDetail.sections.overview.mainText) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Features Title</label>
<input type="text" class="form-control" data-section="overview" data-field="featuresTitle"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.overview && data.campDetail.sections.overview.featuresTitle) || '' %>"
placeholder="e.g., Key features">
</div>
<div class="col-12">
<label class="form-label fw-medium">Key Features</label>
<div class="features-editor">
<div id="overviewFeaturesList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addFeature('overview')">
<i class="fas fa-plus me-1"></i>Add Feature
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Feature Image</label>
<div class="input-group">
<input type="text" class="form-control" data-section="overview" data-field="featureImage"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.overview && data.campDetail.sections.overview.featureImage) || '' %>"
placeholder="/templates/yootheme/activities/activity-details/bg-ad4.png">
<button class="btn btn-outline-secondary" type="button" onclick="selectImage(this)">
<i class="fas fa-image"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Location Section -->
<div class="tab-pane fade" id="location-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-map-marker-alt me-2"></i>Location Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" data-section="location" data-field="title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.location && data.campDetail.sections.location.title) || '' %>"
placeholder="e.g., Location">
</div>
<div class="col-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" data-section="location" data-field="description" rows="5"
placeholder="Describe the camp location and surrounding area..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.location && data.campDetail.sections.location.description) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Location Images</label>
<div class="location-images-editor">
<div id="locationImagesList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addLocationImage()">
<i class="fas fa-plus me-1"></i>Add Image
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Accommodation Section -->
<div class="tab-pane fade" id="accommodation-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-bed me-2"></i>Accommodation Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Hero Image</label>
<div class="input-group">
<input type="text" class="form-control" data-section="accommodation" data-field="heroImage"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.accommodation && data.campDetail.sections.accommodation.heroImage) || '' %>"
placeholder="/templates/yootheme/cache/...">
<button class="btn btn-outline-secondary" type="button" onclick="selectImage(this)">
<i class="fas fa-image"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" data-section="accommodation" data-field="title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.accommodation && data.campDetail.sections.accommodation.title) || '' %>"
placeholder="e.g., Accommodation Options">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" data-section="accommodation" data-field="subtitle"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.accommodation && data.campDetail.sections.accommodation.subtitle) || '' %>"
placeholder="e.g., Camp Life Like a Little Village!">
</div>
<div class="col-12">
<label class="form-label fw-medium">Quote</label>
<input type="text" class="form-control" data-section="accommodation" data-field="quote"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.accommodation && data.campDetail.sections.accommodation.quote) || '' %>"
placeholder="Optional quote text">
</div>
<div class="col-12">
<label class="form-label fw-medium">Main Heading</label>
<input type="text" class="form-control" data-section="accommodation" data-field="mainHeading"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.accommodation && data.campDetail.sections.accommodation.mainHeading) || '' %>"
placeholder="Optional main heading">
</div>
<div class="col-12">
<label class="form-label fw-medium">Introduction Text</label>
<div class="intro-text-editor">
<div id="accommodationIntroList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('accommodation', 'intro')">
<i class="fas fa-plus me-1"></i>Add Intro Paragraph
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Outro Text</label>
<div class="outro-text-editor">
<div id="accommodationOutroList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('accommodation', 'outro')">
<i class="fas fa-plus me-1"></i>Add Outro Paragraph
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Details</label>
<div class="details-editor">
<div id="accommodationDetailsList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('accommodation', 'details')">
<i class="fas fa-plus me-1"></i>Add Detail
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Age Principles</label>
<div class="principles-editor">
<div id="accommodationPrinciplesList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('accommodation', 'principles')">
<i class="fas fa-plus me-1"></i>Add Principle
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Program Section -->
<div class="tab-pane fade" id="program-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-calendar-week me-2"></i>Program Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Hero Image</label>
<div class="input-group">
<input type="text" class="form-control" data-section="program" data-field="heroImage"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.program && data.campDetail.sections.program.heroImage) || '' %>"
placeholder="/templates/yootheme/cache/...">
<button class="btn btn-outline-secondary" type="button" onclick="selectImage(this)">
<i class="fas fa-image"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" data-section="program" data-field="title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.program && data.campDetail.sections.program.title) || '' %>"
placeholder="e.g., Program">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" data-section="program" data-field="subtitle"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.program && data.campDetail.sections.program.subtitle) || '' %>"
placeholder="e.g., A Full Day of Adventure, Sports & Creativity!">
</div>
<div class="col-12">
<label class="form-label fw-medium">Quote</label>
<input type="text" class="form-control" data-section="program" data-field="quote"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.program && data.campDetail.sections.program.quote) || '' %>"
placeholder="Optional quote text">
</div>
<div class="col-12">
<label class="form-label fw-medium">Main Heading</label>
<input type="text" class="form-control" data-section="program" data-field="mainHeading"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.program && data.campDetail.sections.program.mainHeading) || '' %>"
placeholder="e.g., Every morning, you get to pick a new exciting activity...">
</div>
<div class="col-12">
<label class="form-label fw-medium">Introduction Text</label>
<div class="intro-text-editor">
<div id="programIntroList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('program', 'intro')">
<i class="fas fa-plus me-1"></i>Add Intro Paragraph
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Outro Text</label>
<div class="outro-text-editor">
<div id="programOutroList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('program', 'outro')">
<i class="fas fa-plus me-1"></i>Add Outro Paragraph
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Principles</label>
<div class="principles-editor">
<div id="programPrinciplesList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('program', 'principles')">
<i class="fas fa-plus me-1"></i>Add Principle
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Footer Text</label>
<div class="footer-text-editor">
<div id="programFooterList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('program', 'footer')">
<i class="fas fa-plus me-1"></i>Add Footer Paragraph
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Meals Section -->
<div class="tab-pane fade" id="meals-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-utensils me-2"></i>Meals Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" data-section="meals" data-field="title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.meals && data.campDetail.sections.meals.title) || '' %>"
placeholder="e.g., Meal On Site">
</div>
<div class="col-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" data-section="meals" data-field="description" rows="3"
placeholder="General description of meal services..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.meals && data.campDetail.sections.meals.description) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Meal Items</label>
<div class="meals-items-editor">
<div id="mealsItemsList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addMealItem()">
<i class="fas fa-plus me-1"></i>Add Meal Item
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Footer Text</label>
<textarea class="form-control" data-section="meals" data-field="footer" rows="3"
placeholder="Footer text for meals section..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.meals && data.campDetail.sections.meals.footer) || '' %></textarea>
</div>
</div>
</div>
</div>
</div>
<!-- Team Section -->
<div class="tab-pane fade" id="team-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-users me-2"></i>Team Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Hero Image</label>
<div class="input-group">
<input type="text" class="form-control" data-section="team" data-field="heroImage"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.team && data.campDetail.sections.team.heroImage) || '' %>"
placeholder="/templates/yootheme/cache/...">
<button class="btn btn-outline-secondary" type="button" onclick="selectImage(this)">
<i class="fas fa-image"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" data-section="team" data-field="title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.team && data.campDetail.sections.team.title) || '' %>"
placeholder="e.g., Team and Supervision">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Subtitle</label>
<input type="text" class="form-control" data-section="team" data-field="subtitle"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.team && data.campDetail.sections.team.subtitle) || '' %>"
placeholder="e.g., Cared for Around the Clock!">
</div>
<div class="col-12">
<label class="form-label fw-medium">Quote</label>
<input type="text" class="form-control" data-section="team" data-field="quote"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.team && data.campDetail.sections.team.quote) || '' %>"
placeholder="Optional quote text">
</div>
<div class="col-12">
<label class="form-label fw-medium">Main Heading</label>
<input type="text" class="form-control" data-section="team" data-field="mainHeading"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.team && data.campDetail.sections.team.mainHeading) || '' %>"
placeholder="Optional main heading">
</div>
<div class="col-12">
<label class="form-label fw-medium">Introduction Text</label>
<div class="intro-text-editor">
<div id="teamIntroList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('team', 'intro')">
<i class="fas fa-plus me-1"></i>Add Intro Paragraph
</button>
</div>
</div>
<div class="col-12">
<label class="form-label fw-medium">Footer Text</label>
<div class="footer-text-editor">
<div id="teamFooterList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTextItem('team', 'footer')">
<i class="fas fa-plus me-1"></i>Add Footer Paragraph
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Insurance Section -->
<div class="tab-pane fade" id="insurance-content" role="tabpanel">
<div class="card border-0 bg-light">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-shield-alt me-2"></i>Insurance Section</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" data-section="insurance" data-field="title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.insurance && data.campDetail.sections.insurance.title) || '' %>"
placeholder="e.g., Coverage and Insurance">
</div>
<div class="col-12">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" data-section="insurance" data-field="description" rows="4"
placeholder="General description of insurance coverage..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.insurance && data.campDetail.sections.insurance.description) || '' %></textarea>
</div>
<!-- Insurance Package -->
<div class="col-12">
<div class="border rounded p-3 mb-3">
<h6 class="fw-bold mb-3"><i class="fas fa-box me-2"></i>Insurance Package</h6>
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Package Title</label>
<input type="text" class="form-control" data-section="insurance" data-field="package.title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.insurance && data.campDetail.sections.insurance.package && data.campDetail.sections.insurance.package.title) || '' %>"
placeholder="e.g., Camp Insurance Package">
</div>
<div class="col-12">
<label class="form-label fw-medium">Package Description</label>
<textarea class="form-control" data-section="insurance" data-field="package.desc" rows="2"
placeholder="Description of the insurance package..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.insurance && data.campDetail.sections.insurance.package && data.campDetail.sections.insurance.package.desc) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Package Items</label>
<div class="package-items-editor">
<div id="insurancePackageItemsList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addInsuranceItem('package')">
<i class="fas fa-plus me-1"></i>Add Package Item
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Cancellation Section -->
<div class="col-12">
<div class="border rounded p-3">
<h6 class="fw-bold mb-3"><i class="fas fa-undo me-2"></i>Cancellation Policy</h6>
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Cancellation Title</label>
<input type="text" class="form-control" data-section="insurance" data-field="cancellation.title"
value="<%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.insurance && data.campDetail.sections.insurance.cancellation && data.campDetail.sections.insurance.cancellation.title) || '' %>"
placeholder="e.g., Travel Cancellation Guarantee">
</div>
<div class="col-12">
<label class="form-label fw-medium">Cancellation Description</label>
<textarea class="form-control" data-section="insurance" data-field="cancellation.desc" rows="2"
placeholder="Description of cancellation policy..."><%= (data.campDetail && data.campDetail.sections && data.campDetail.sections.insurance && data.campDetail.sections.insurance.cancellation && data.campDetail.sections.insurance.cancellation.desc) || '' %></textarea>
</div>
<div class="col-12">
<label class="form-label fw-medium">Cancellation Items</label>
<div class="cancellation-items-editor">
<div id="insuranceCancellationItemsList" class="mb-3"></div>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addInsuranceItem('cancellation')">
<i class="fas fa-plus me-1"></i>Add Cancellation Item
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden textarea for JSON synchronization -->
<textarea class="form-control d-none font-monospace" name="campDetailSections" id="sectionsJson" rows="25"><%= JSON.stringify((data.campDetail && data.campDetail.sections) || (data['camp-detail'] && data['camp-detail'].sections) || {}, null, 2) %></textarea>
</div>
</div>
</div>
<!-- Advanced JSON Editor removed; using visual section editors and hidden JSON for sync -->
</div>
</div>
</div>
<!-- Booking Sessions -->
<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-calendar-alt me-2"></i>Booking Sessions</h5>
<button type="button" class="btn btn-primary btn-sm" onclick="addBookingSession()">
<i class="fas fa-plus me-1"></i>Add Session
</button>
</div>
<div class="card-body">
<div id="bookingSessionsList">
<!-- Sessions will be rendered here by JavaScript -->
</div>
<div id="noSessionsMessage" class="text-center text-muted py-4" style="display: none;">
<i class="fas fa-calendar-plus fa-3x mb-3 text-secondary"></i>
<p class="mb-0">No booking sessions yet. Click "Add Session" to create one.</p>
</div>
<!-- Hidden textarea for JSON data -->
<textarea name="bookingSessions" id="bookingSessionsJson" class="d-none"><%= JSON.stringify(data.bookingSessions || []) %></textarea>
</div>
</div>
<% if (isEdit && data._id) { %>
<!-- Booking Submissions -->
<div id="bookings" 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-users me-2"></i>Booking Submissions</h5>
<button type="button" class="btn btn-info btn-sm" id="viewBookingsBtn" onclick="loadBookingSubmissions('<%= data._id %>')">
<i class="fas fa-eye me-1"></i>View Bookings (<span id="bookingCountBtn">Loading...</span>)
</button>
</div>
<div class="card-body" id="bookingSubmissionsSection" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h6 class="mb-1">Booking Statistics</h6>
<p class="text-muted small mb-0">Overview of all booking submissions for this activity</p>
</div>
<div class="text-end">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="exportBookingData('<%= data._id %>')">
<i class="fas fa-download me-1"></i>Export CSV
</button>
<button type="button" class="btn btn-outline-secondary btn-sm ms-2" onclick="refreshBookingData('<%= data._id %>')">
<i class="fas fa-sync me-1"></i>Refresh
</button>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4" id="bookingStats">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<i class="fas fa-clipboard-list fa-2x mb-2"></i>
<h3 class="mb-0" id="totalBookings">0</h3>
<p class="mb-0">Total Bookings</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<i class="fas fa-check-circle fa-2x mb-2"></i>
<h3 class="mb-0" id="confirmedBookings">0</h3>
<p class="mb-0">Confirmed</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<i class="fas fa-clock fa-2x mb-2"></i>
<h3 class="mb-0" id="pendingBookings">0</h3>
<p class="mb-0">Pending</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<i class="fas fa-dollar-sign fa-2x mb-2"></i>
<h3 class="mb-0" id="totalRevenue">$0</h3>
<p class="mb-0">Total Revenue</p>
</div>
</div>
</div>
</div>
<!-- Sessions Breakdown -->
<div id="sessionBreakdown" class="mb-4">
<h6 class="mb-3">Sessions Breakdown</h6>
<div id="sessionBreakdownContent">
<!-- Session breakdown will be loaded here -->
</div>
</div>
<!-- Filter Controls -->
<div class="row mb-3">
<div class="col-md-3">
<select class="form-select" id="statusFilter" onchange="filterBookings()">
<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>
</div>
<div class="col-md-3">
<select class="form-select" id="sessionFilter" onchange="filterBookings()">
<option value="">All Sessions</option>
<!-- Session options will be populated -->
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="paymentFilter" onchange="filterBookings()">
<option value="">All Payment Status</option>
<option value="pending">Payment Pending</option>
<option value="partial">Partial Payment</option>
<option value="paid">Paid</option>
<option value="refunded">Refunded</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="searchFilter" placeholder="Search by name/email..." onkeyup="filterBookings()">
</div>
</div>
<!-- Bookings Table -->
<div class="table-responsive">
<table class="table table-hover table-sm">
<thead class="table-light">
<tr>
<th width="80">Date</th>
<th width="120">Session</th>
<th width="150">Participant</th>
<th width="150">Parent/Guardian</th>
<th width="200">Contact</th>
<th width="80">Gender/Age</th>
<th width="100">Status</th>
<th width="100">Payment</th>
<th width="120">Actions</th>
</tr>
</thead>
<tbody id="bookingTableBody">
<tr>
<td colspan="9" class="text-center py-4">
<i class="fas fa-spinner fa-spin me-2"></i>Loading bookings...
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<nav aria-label="Bookings pagination" id="bookingPagination" style="display: none;">
<ul class="pagination justify-content-center" id="paginationList">
<!-- Pagination will be loaded here -->
</ul>
</nav>
</div>
</div>
<% } %>
<!-- Fixed Bottom Buttons (Save / Reset) -->
<div class="fixed-bottom-buttons">
<button type="button" class="btn btn-secondary" id="activityResetBtn">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="activitySubmitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
<!-- Booking Details Modal -->
<div class="modal fade" id="bookingDetailsModal" tabindex="-1" aria-labelledby="bookingDetailsModalLabel" 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 bg-info text-white">
<h5 class="modal-title" id="bookingDetailsModalLabel"><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="bookingDetailsContent">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-warning" id="editBookingFromDetailsBtn">
<i class="fas fa-edit me-1"></i>Edit Booking
</button>
</div>
</div>
</div>
</div>
<!-- Edit Booking Modal -->
<div class="modal fade" id="editBookingModal" tabindex="-1" aria-labelledby="editBookingModalLabel" 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 bg-warning">
<h5 class="modal-title" id="editBookingModalLabel"><i class="fas fa-edit me-2"></i>Edit Booking</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="editBookingContent">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="saveBookingBtn">
<i class="fas fa-save me-1"></i>Save Changes
</button>
</div>
</div>
</div>
</div>
<!-- Delete Booking Confirmation Modal -->
<div class="modal fade" id="deleteBookingModal" tabindex="-1" aria-labelledby="deleteBookingModalLabel" 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 bg-danger text-white">
<h5 class="modal-title" id="deleteBookingModalLabel"><i class="fas fa-trash me-2"></i>Confirm Delete</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the booking for "<span id="deleteBookingName" class="fw-bold"></span>"?</p>
<p class="text-danger mb-0"><small><i class="fas fa-exclamation-triangle me-1"></i>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>
<button type="button" class="btn btn-danger" id="confirmDeleteBookingBtn">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
<!-- Success/Error Toast for Booking Actions -->
<div class="modal fade" id="bookingResultModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header" id="bookingResultHeader">
<h5 class="modal-title" id="bookingResultTitle">Result</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center" id="bookingResultBody">
<p id="bookingResultMessage"></p>
</div>
<div class="modal-footer justify-content-center">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<style>
/* Fix modal z-index - must be higher than everything else */
#bookingDetailsModal,
#editBookingModal,
#deleteBookingModal,
#bookingResultModal {
z-index: 2050 !important;
}
#bookingDetailsModal .modal-backdrop,
#editBookingModal .modal-backdrop,
#deleteBookingModal .modal-backdrop,
#bookingResultModal .modal-backdrop {
z-index: 2040 !important;
}
#bookingDetailsModal .modal-dialog,
#editBookingModal .modal-dialog,
#deleteBookingModal .modal-dialog,
#bookingResultModal .modal-dialog {
z-index: 2060 !important;
}
/* Ensure modal content is visible */
#bookingDetailsModal .modal-content,
#editBookingModal .modal-content,
#deleteBookingModal .modal-content,
#bookingResultModal .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 */
#bookingDetailsModal,
#editBookingModal,
#deleteBookingModal,
#bookingResultModal {
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 */
#bookingDetailsModal.show,
#editBookingModal.show,
#deleteBookingModal.show,
#bookingResultModal.show {
background-color: rgba(0, 0, 0, 0.5) !important;
}
/* Center the modal dialog */
#bookingDetailsModal .modal-dialog,
#editBookingModal .modal-dialog,
#deleteBookingModal .modal-dialog,
#bookingResultModal .modal-dialog {
display: flex;
align-items: center;
min-height: calc(100% - 1rem);
margin: 0.5rem auto;
}
/* Booking details modal specific styling */
#bookingDetailsModal .modal-body {
max-height: 70vh;
overflow-y: auto;
}
#bookingDetailsModal .card {
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#bookingDetailsModal .card-header {
border-bottom: 1px solid rgba(255,255,255,0.2);
font-weight: 600;
}
/* Result modal styling */
#bookingResultModal.success .modal-header {
background-color: #198754;
color: white;
}
#bookingResultModal.error .modal-header {
background-color: #dc3545;
color: white;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function () {
const form = document.getElementById('activityForm');
const submitBtn = document.getElementById('activitySubmitBtn');
const resetBtn = document.getElementById('activityResetBtn');
if (form && submitBtn) {
form.addEventListener('submit', function () {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
});
}
if (resetBtn) {
resetBtn.addEventListener('click', function () {
if (confirm('Are you sure you want to reset all changes?')) {
// simple approach: reload page to restore server state
window.location.reload();
}
});
}
});
</script>
<!-- Sidebar -->
<div class="col-md-4">
<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-image me-2"></i>Image</h6>
</div>
<div class="card-body text-center">
<%
// Hiển thị camp image: data.image và campDetail.hero.bgImage luôn đồng bộ
// Ưu tiên data.image (main field), fallback campDetail/camp-detail hero bgImage
let tipImg = (data && data.image) || (data && data.campDetail && data.campDetail.hero && data.campDetail.hero.bgImage) || (data && data['camp-detail'] && data['camp-detail'].hero && data['camp-detail'].hero.bgImage) || '/images/placeholder.png';
let tipImgSrc = (tipImg && tipImg.startsWith && (tipImg.startsWith('http') || tipImg.startsWith('/'))) ? tipImg : ('/uploads/activity/' + tipImg);
%>
<div id="tipsImagePreview" class="mb-2">
<img id="tipsPreviewImg" src="<%= tipImgSrc %>" alt="Camp Image" class="img-fluid rounded" style="max-height:200px; object-fit:cover; width:100%">
</div>
<div class="input-group mt-2">
<input type="text" class="form-control" name="tipsImage" id="tipsImageInput" value="<%= tipImg %>" placeholder="Image path or URL">
<button type="button" class="btn btn-outline-secondary" id="uploadTipsImageBtn"><i class="fas fa-upload me-1"></i>Upload</button>
</div>
<small class="text-muted d-block mt-1">Camp image (synchronized with camp record)</small>
</div>
</div>
<% if (isEdit) { %>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom">
<h6 class="mb-0"><i class="fas fa-cog me-2"></i>Settings</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-medium">Display Order</label>
<input type="number" class="form-control" name="order" value="<%= data.order || 0 %>" min="0">
<small class="text-muted">Lower numbers appear first</small>
</div>
<div class="mb-3">
<label class="form-label fw-medium">Status</label>
<div class="form-check form-switch mt-2">
<!-- Hidden input holds the actual value sent to server; synced on submit -->
<input type="hidden" name="isActive" id="isActiveHidden" value="<%= data.isActive !== false ? 'true' : 'false' %>">
<!-- Visual checkbox (no name) controls the hidden input -->
<input class="form-check-input" type="checkbox" id="isActive" <%= data.isActive !== false ? 'checked' : '' %>>
<label class="form-check-label" for="isActive">Active</label>
</div>
</div>
<hr>
<table class="table table-sm table-borderless mb-0">
<tr>
<td class="text-muted">ID:</td>
<td><code><%= data._id %></code></td>
</tr>
<% if (data.createdAt) { %>
<tr>
<td class="text-muted">Created:</td>
<td>
<%= new Date(data.createdAt).toLocaleDateString('vi-VN') %>
</td>
</tr>
<% } %>
<% if (data.updatedAt) { %>
<tr>
<td class="text-muted">Updated:</td>
<td>
<%= new Date(data.updatedAt).toLocaleDateString('vi-VN') %>
</td>
</tr>
<% } %>
</table>
</div>
</div>
<% } %>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Ensure `form` is available to all handlers (avoid TDZ ReferenceError)
const form = document.getElementById('activityForm');
// Image input removed: use Tips image in sidebar instead
// Tips image upload (sidebar)
const uploadTipsBtn = document.getElementById('uploadTipsImageBtn');
const tipsImageInput = document.getElementById('tipsImageInput');
const tipsImagePreview = document.getElementById('tipsImagePreview');
const tipsPreviewImg = document.getElementById('tipsPreviewImg');
const imageInput = document.getElementById('imageInput');
// expose current camp link (without leading slash) for JSON updates
const currentCampLink = "<%= ((data.link||'').replace(/^\//,'')) %>";
if (uploadTipsBtn) {
uploadTipsBtn.addEventListener('click', function () {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
if (currentCampLink) {
formData.append('jsonFile', 'activities.json');
formData.append('campLink', currentCampLink);
}
uploadTipsBtn.disabled = true;
uploadTipsBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
try {
const response = await fetch('/admin/upload/image?imageType=activity', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
const p = result.path || result.url || '';
// update sidebar input, main image input, and preview
try { tipsImageInput.value = p; } catch (e) {}
try { if (imageInput) imageInput.value = p; } catch (e) {}
const previewUrl = p && (p.startsWith('/') || p.startsWith('http')) ? p : ('/uploads/' + p);
tipsPreviewImg.src = previewUrl;
tipsImagePreview.classList.remove('d-none');
showToast('Success', 'Image uploaded successfully', 'success');
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', error.message || 'Upload failed', 'error');
} finally {
uploadTipsBtn.disabled = false;
uploadTipsBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
}
};
fileInput.click();
});
}
// Menu Items editor (sidebar)
const menuItemsEditor = document.getElementById('menuItemsEditor');
const menuItemsTextarea = document.getElementById('campDetailSidebarMenuItems');
const addMenuItemBtn = document.getElementById('addMenuItemBtn');
function parseMenuItems() {
if (!menuItemsTextarea) return [];
try {
const v = menuItemsTextarea.value || '[]';
return JSON.parse(v);
} catch (e) {
console.warn('Invalid menuItems JSON, using empty array', e);
return [];
}
}
function renderMenuItems(items) {
if (!menuItemsEditor) return;
menuItemsEditor.innerHTML = '';
items.forEach((it, idx) => {
const row = document.createElement('div');
row.className = 'd-flex gap-2 align-items-center mb-2';
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'form-control form-control-sm';
nameInput.placeholder = 'Name';
nameInput.value = it.name || '';
nameInput.style.maxWidth = '45%';
const hrefInput = document.createElement('input');
hrefInput.type = 'text';
hrefInput.className = 'form-control form-control-sm';
hrefInput.placeholder = 'href (e.g. #overview)';
hrefInput.value = it.href || '';
hrefInput.style.maxWidth = '45%';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-sm btn-outline-danger';
removeBtn.innerHTML = '<i class="fas fa-trash"></i>';
removeBtn.addEventListener('click', () => {
const cur = parseMenuItems();
cur.splice(idx, 1);
syncMenuItems(cur);
renderMenuItems(cur);
});
nameInput.addEventListener('input', () => {
const cur = parseMenuItems();
cur[idx] = cur[idx] || {};
cur[idx].name = nameInput.value;
syncMenuItems(cur);
});
hrefInput.addEventListener('input', () => {
const cur = parseMenuItems();
cur[idx] = cur[idx] || {};
cur[idx].href = hrefInput.value;
syncMenuItems(cur);
});
row.appendChild(nameInput);
row.appendChild(hrefInput);
row.appendChild(removeBtn);
menuItemsEditor.appendChild(row);
});
if (items.length === 0) {
const hint = document.createElement('div');
hint.className = 'text-muted small';
hint.textContent = 'No menu items yet. Click Add Menu Item to create one.';
menuItemsEditor.appendChild(hint);
}
}
function syncMenuItems(items) {
const arr = Array.isArray(items) ? items : parseMenuItems();
if (!menuItemsTextarea) return;
menuItemsTextarea.value = JSON.stringify(arr, null, 2);
}
if (addMenuItemBtn) {
addMenuItemBtn.addEventListener('click', () => {
const cur = parseMenuItems();
cur.push({ name: 'New Item', href: '#new' });
syncMenuItems(cur);
renderMenuItems(cur);
// focus last name input
setTimeout(() => {
const inputs = menuItemsEditor.querySelectorAll('input');
if (inputs && inputs.length) inputs[inputs.length - 2].focus();
}, 50);
});
}
// Initialize menu items editor from textarea
try {
const initialMenuItems = parseMenuItems();
renderMenuItems(initialMenuItems);
} catch (e) {
renderMenuItems([]);
}
// Ensure menu items sync on form submit
if (form) {
form.addEventListener('submit', function () {
const curInputs = menuItemsEditor ? Array.from(menuItemsEditor.querySelectorAll('div.d-flex')) : [];
// Already synced on input/remove/add; ensure textarea contains latest
// (no-op here, kept for clarity)
try { menuItemsTextarea && (menuItemsTextarea.value = menuItemsTextarea.value); } catch (e) {}
});
}
// Main Gallery editor
const mainGalleryEditor = document.getElementById('mainGalleryEditor');
const mainGalleryTextarea = document.getElementById('campDetailMainGallerySlides');
const addMainGallerySlideBtn = document.getElementById('addMainGallerySlideBtn');
function parseGallerySlides() {
if (!mainGalleryTextarea) return [];
try {
return JSON.parse(mainGalleryTextarea.value || '[]');
} catch (e) {
console.warn('Invalid gallery slides JSON, using empty array', e);
return [];
}
}
function syncGallerySlides(items) {
if (!mainGalleryTextarea) return;
mainGalleryTextarea.value = JSON.stringify(Array.isArray(items) ? items : parseGallerySlides(), null, 2);
}
function renderGallerySlides(items) {
if (!mainGalleryEditor) return;
mainGalleryEditor.innerHTML = '';
// Render each slide full-width with controls below the image
items.forEach((it, idx) => {
const row = document.createElement('div');
row.className = 'mb-3 p-2 border rounded';
// Thumbnail (full width)
const thumb = document.createElement('img');
thumb.style.width = '100%';
thumb.style.height = 'auto';
thumb.style.objectFit = 'cover';
thumb.style.borderRadius = '4px';
thumb.alt = it.alt || '';
if (it.url) {
let src = it.url;
if (!/^https?:\/\//i.test(src) && !/^data:/i.test(src)) {
if (src.startsWith('/')) src = window.location.origin + src;
else src = window.location.origin + '/' + src;
}
thumb.src = src;
} else {
thumb.src = '/images/placeholder.png';
}
// Bottom row: fields + actions
const bottom = document.createElement('div');
bottom.className = 'd-flex align-items-start gap-3 mt-2';
// Fields container
const fields = document.createElement('div');
fields.className = 'flex-grow-1';
const altRow = document.createElement('div');
altRow.className = 'mb-1';
const altLabel = document.createElement('label');
altLabel.className = 'form-label small mb-0';
altLabel.textContent = 'Alt';
const altInput = document.createElement('input');
altInput.type = 'text';
altInput.className = 'form-control form-control-sm';
altInput.value = it.alt || '';
altRow.appendChild(altLabel);
altRow.appendChild(altInput);
const urlRow = document.createElement('div');
urlRow.className = 'mb-1';
const urlLabel = document.createElement('label');
urlLabel.className = 'form-label small mb-0';
urlLabel.textContent = 'URL';
const urlInput = document.createElement('input');
urlInput.type = 'text';
urlInput.className = 'form-control form-control-sm';
urlInput.placeholder = '/path/to/image.jpg or https://...';
urlInput.value = it.url || '';
urlRow.appendChild(urlLabel);
urlRow.appendChild(urlInput);
fields.appendChild(altRow);
fields.appendChild(urlRow);
// Actions
const actions = document.createElement('div');
actions.className = 'd-flex flex-column align-items-end gap-2';
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-sm btn-outline-secondary';
uploadBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-sm btn-outline-danger';
removeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove';
actions.appendChild(uploadBtn);
actions.appendChild(removeBtn);
bottom.appendChild(fields);
bottom.appendChild(actions);
row.appendChild(thumb);
row.appendChild(bottom);
mainGalleryEditor.appendChild(row);
function updateSlide() {
const cur = parseGallerySlides();
cur[idx] = cur[idx] || {};
cur[idx].alt = altInput.value;
cur[idx].url = urlInput.value;
syncGallerySlides(cur);
}
altInput.addEventListener('input', updateSlide);
urlInput.addEventListener('input', () => {
updateSlide();
try {
const v = urlInput.value.trim();
if (v) {
let s = v;
if (!/^https?:\/\//i.test(s) && !/^data:/i.test(s)) {
if (s.startsWith('/')) s = window.location.origin + s;
else s = window.location.origin + '/' + s;
}
thumb.src = s;
} else thumb.src = '/images/placeholder.png';
} catch (e) { console.warn(e); }
});
removeBtn.addEventListener('click', () => {
const cur = parseGallerySlides();
cur.splice(idx, 1);
syncGallerySlides(cur);
renderGallerySlides(cur);
});
uploadBtn.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const fd = new FormData();
fd.append('image', file);
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
try {
const resp = await fetch('/admin/upload/image?imageType=activity', { method: 'POST', body: fd });
const result = await resp.json();
if (result.success) {
urlInput.value = result.path;
updateSlide();
thumb.src = result.url || (window.location.origin + result.path);
showToast('Success', 'Image uploaded', 'success');
} else throw new Error(result.error || 'Upload failed');
} catch (err) {
console.error(err);
showToast('Error', err.message || 'Upload failed', 'error');
} finally {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
}
};
fileInput.click();
});
});
if (items.length === 0) {
const hint = document.createElement('div');
hint.className = 'text-muted small';
hint.textContent = 'No slides yet. Click Add Slide to create one.';
mainGalleryEditor.appendChild(hint);
}
}
if (addMainGallerySlideBtn) {
addMainGallerySlideBtn.addEventListener('click', () => {
const cur = parseGallerySlides();
const newItem = { url: '', alt: '' };
cur.push(newItem);
syncGallerySlides(cur);
renderGallerySlides(cur);
});
}
try {
const initialSlides = parseGallerySlides();
renderGallerySlides(initialSlides);
} catch (e) {
renderGallerySlides([]);
}
// Ensure gallery sync on submit
if (form) {
form.addEventListener('submit', function () {
try { mainGalleryTextarea && (mainGalleryTextarea.value = mainGalleryTextarea.value); } catch (e) {}
});
}
// Upcoming Tours editor (sidebar) - add/edit/remove + image upload
const upcomingEditor = document.getElementById('upcomingToursEditor');
const upcomingTextarea = document.getElementById('campDetailSidebarUpcomingTours');
const addUpcomingBtn = document.getElementById('addUpcomingTourBtn');
function parseUpcomingTours() {
if (!upcomingTextarea) return [];
try {
const v = upcomingTextarea.value || '[]';
const parsed = JSON.parse(v);
console.log('Parsed upcoming tours from textarea:', parsed.length, 'items', parsed);
return parsed;
} catch (e) {
console.warn('Invalid upcomingTours JSON:', upcomingTextarea.value, 'Error:', e);
return [];
}
}
function syncUpcomingTours(items) {
if (!upcomingTextarea) return;
upcomingTextarea.value = JSON.stringify(items, null, 2);
}
function renderUpcomingTours(items) {
console.log('=== renderUpcomingTours called ===');
console.log('upcomingEditor element:', upcomingEditor);
console.log('items to render:', items);
if (!upcomingEditor) {
console.error('upcomingEditor is null, cannot render');
return;
}
upcomingEditor.innerHTML = '';
console.log('Rendering upcoming tours:', items.length, 'items');
items.forEach((it, originalIdx) => {
const card = document.createElement('div');
card.className = 'card card-body mb-2 p-2';
const row = document.createElement('div');
row.className = 'row g-2 align-items-center';
const idCol = document.createElement('div');
idCol.className = 'col-12 col-md-2';
const idLabel = document.createElement('label');
idLabel.className = 'form-label small mb-1';
idLabel.textContent = 'ID';
const idInput = document.createElement('input');
idInput.type = 'text';
idInput.className = 'form-control form-control-sm';
idInput.placeholder = 'id';
idInput.value = it.id || '';
idCol.appendChild(idLabel);
idCol.appendChild(idInput);
const titleCol = document.createElement('div');
titleCol.className = 'col-12 col-md-4';
const titleLabel = document.createElement('label');
titleLabel.className = 'form-label small mb-1';
titleLabel.textContent = 'Title';
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.className = 'form-control form-control-sm';
titleInput.placeholder = 'Title';
titleInput.value = it.title || '';
titleCol.appendChild(titleLabel);
titleCol.appendChild(titleInput);
const ratingCol = document.createElement('div');
ratingCol.className = 'col-6 col-md-2';
const ratingLabel = document.createElement('label');
ratingLabel.className = 'form-label small mb-1';
ratingLabel.textContent = 'Rating';
const ratingInput = document.createElement('input');
ratingInput.type = 'number';
ratingInput.step = '0.1';
ratingInput.min = '0';
ratingInput.max = '5';
ratingInput.className = 'form-control form-control-sm';
ratingInput.placeholder = 'Rating';
ratingInput.value = it.rating || '';
ratingCol.appendChild(ratingLabel);
ratingCol.appendChild(ratingInput);
const reviewsCol = document.createElement('div');
reviewsCol.className = 'col-6 col-md-2';
const reviewsLabel = document.createElement('label');
reviewsLabel.className = 'form-label small mb-1';
reviewsLabel.textContent = 'Reviews';
const reviewsInput = document.createElement('input');
reviewsInput.type = 'number';
reviewsInput.min = '0';
reviewsInput.className = 'form-control form-control-sm';
reviewsInput.placeholder = 'Reviews';
reviewsInput.value = it.reviews || '';
reviewsCol.appendChild(reviewsLabel);
reviewsCol.appendChild(reviewsInput);
const locCol = document.createElement('div');
locCol.className = 'col-12 col-md-6';
const locLabel = document.createElement('label');
locLabel.className = 'form-label small mb-1';
locLabel.textContent = 'Location';
const locInput = document.createElement('input');
locInput.type = 'text';
locInput.className = 'form-control form-control-sm';
locInput.placeholder = 'Location';
locInput.value = it.location || '';
locCol.appendChild(locLabel);
locCol.appendChild(locInput);
const priceCol = document.createElement('div');
priceCol.className = 'col-6 col-md-3';
const priceLabel = document.createElement('label');
priceLabel.className = 'form-label small mb-1';
priceLabel.textContent = 'Price';
const priceInput = document.createElement('input');
priceInput.type = 'number';
priceInput.min = '0';
priceInput.step = '0.01';
priceInput.className = 'form-control form-control-sm';
priceInput.placeholder = 'Price';
priceInput.value = it.price || '';
priceCol.appendChild(priceLabel);
priceCol.appendChild(priceInput);
const origPriceCol = document.createElement('div');
origPriceCol.className = 'col-6 col-md-3';
const origPriceLabel = document.createElement('label');
origPriceLabel.className = 'form-label small mb-1';
origPriceLabel.textContent = 'Original Price';
const origPriceInput = document.createElement('input');
origPriceInput.type = 'number';
origPriceInput.min = '0';
origPriceInput.step = '0.01';
origPriceInput.className = 'form-control form-control-sm';
origPriceInput.placeholder = 'Original Price';
origPriceInput.value = it.originalPrice || '';
origPriceCol.appendChild(origPriceLabel);
origPriceCol.appendChild(origPriceInput);
const imageCol = document.createElement('div');
imageCol.className = 'col-12 col-md-6 d-flex gap-2 align-items-center';
const imageLabel = document.createElement('label');
imageLabel.className = 'form-label small mb-1';
imageLabel.textContent = 'Image';
const imageInput = document.createElement('input');
imageInput.type = 'text';
imageInput.className = 'form-control form-control-sm';
imageInput.placeholder = 'Image URL or path';
imageInput.value = it.image || '';
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-sm btn-outline-secondary';
uploadBtn.innerHTML = '<i class="fas fa-upload"></i>';
const imgPreview = document.createElement('img');
imgPreview.style.width = '72px';
imgPreview.style.height = '48px';
imgPreview.style.objectFit = 'cover';
imgPreview.style.border = '1px solid #ddd';
imgPreview.style.borderRadius = '4px';
imgPreview.style.display = 'none';
imgPreview.alt = 'Tour preview';
imageCol.appendChild(imgPreview);
// Use a small wrapper to stack label and input when space is limited
const imageWrapper = document.createElement('div');
imageWrapper.className = 'flex-grow-1';
imageWrapper.appendChild(imageLabel);
imageWrapper.appendChild(imageInput);
imageCol.appendChild(imageWrapper);
imageCol.appendChild(uploadBtn);
const actionsCol = document.createElement('div');
actionsCol.className = 'col-12 col-md-12 text-end';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-sm btn-outline-danger';
removeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove';
actionsCol.appendChild(removeBtn);
row.appendChild(idCol);
row.appendChild(titleCol);
row.appendChild(ratingCol);
row.appendChild(reviewsCol);
row.appendChild(locCol);
row.appendChild(priceCol);
row.appendChild(origPriceCol);
row.appendChild(imageCol);
card.appendChild(row);
card.appendChild(actionsCol);
upcomingEditor.appendChild(card);
function updateItem() {
const cur = parseUpcomingTours();
// Always use current length-1 if we can't find it, but try to match by ID first
let targetIdx = originalIdx;
// Try to find by ID (handle both string and number)
if (it.id) {
const foundIdx = cur.findIndex(item =>
String(item.id) === String(it.id)
);
if (foundIdx >= 0) targetIdx = foundIdx;
}
if (targetIdx >= 0 && targetIdx < cur.length) {
cur[targetIdx] = cur[targetIdx] || {};
cur[targetIdx].id = idInput.value || cur[targetIdx].id || Date.now();
cur[targetIdx].title = titleInput.value;
cur[targetIdx].rating = ratingInput.value ? parseFloat(ratingInput.value) : null;
cur[targetIdx].reviews = reviewsInput.value ? parseInt(reviewsInput.value) : 0;
cur[targetIdx].location = locInput.value;
cur[targetIdx].price = priceInput.value ? parseFloat(priceInput.value) : null;
cur[targetIdx].originalPrice = origPriceInput.value ? parseFloat(origPriceInput.value) : null;
cur[targetIdx].image = imageInput.value;
}
// Update preview
try {
const v = (imageInput.value || '').trim();
if (v) {
let src = v;
if (!/^https?:\/\//i.test(src) && !/^data:/i.test(src)) {
if (src.startsWith('/')) src = window.location.origin + src;
else src = window.location.origin + '/' + src;
}
imgPreview.src = src;
imgPreview.style.display = 'inline-block';
} else {
imgPreview.style.display = 'none';
}
} catch (e) {
console.warn('preview update error', e);
}
syncUpcomingTours(cur);
}
// Initialize image preview if data exists
if (it.image) {
try {
let src = it.image;
if (!/^https?:\/\//i.test(src) && !/^data:/i.test(src)) {
if (src.startsWith('/')) src = window.location.origin + src;
else src = window.location.origin + '/' + src;
}
imgPreview.src = src;
imgPreview.style.display = 'inline-block';
} catch (e) {
console.warn('Initial preview error', e);
}
}
[idInput, titleInput, ratingInput, reviewsInput, locInput, priceInput, origPriceInput, imageInput].forEach(inp => {
inp.addEventListener('input', updateItem);
});
removeBtn.addEventListener('click', () => {
const cur = parseUpcomingTours();
let targetIdx = originalIdx;
// Try to find by ID
if (it.id) {
const foundIdx = cur.findIndex(item => String(item.id) === String(it.id));
if (foundIdx >= 0) targetIdx = foundIdx;
}
if (targetIdx >= 0 && targetIdx < cur.length) {
cur.splice(targetIdx, 1);
syncUpcomingTours(cur);
renderUpcomingTours(cur);
}
});
// Upload handler per item
uploadBtn.addEventListener('click', () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const fd = new FormData();
fd.append('image', file);
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const resp = await fetch('/admin/upload/image?imageType=activity', { method: 'POST', body: fd });
const result = await resp.json();
if (result.success) {
imageInput.value = result.path;
updateItem();
showToast('Success', 'Image uploaded', 'success');
const previewUrl = result.url || (window.location.origin + result.path);
imgPreview.src = previewUrl;
imgPreview.style.display = 'inline-block';
} else throw new Error(result.error || 'Upload failed');
} catch (err) {
console.error(err);
showToast('Error', err.message || 'Upload failed', 'error');
} finally {
uploadBtn.disabled = false;
uploadBtn.innerHTML = '<i class="fas fa-upload"></i>';
}
};
fileInput.click();
});
});
if (items.length === 0) {
const hint = document.createElement('div');
hint.className = 'text-muted small';
hint.textContent = 'No upcoming tours yet. Click Add Upcoming Tour to create one.';
upcomingEditor.appendChild(hint);
}
}
if (addUpcomingBtn) {
addUpcomingBtn.addEventListener('click', () => {
console.log('=== Add Upcoming Tour button clicked ===');
const cur = parseUpcomingTours();
console.log('Current upcoming tours:', cur);
const newItem = { id: Date.now(), title: 'New Tour', rating: 4.9, reviews: 0, location: '', price: 0, originalPrice: 0, image: '' };
cur.push(newItem);
console.log('After adding new item:', cur);
syncUpcomingTours(cur);
renderUpcomingTours(cur);
setTimeout(() => {
const inputs = upcomingEditor.querySelectorAll('input');
if (inputs && inputs.length) inputs[0].focus();
}, 50);
});
} else {
console.error('addUpcomingTourBtn not found, cannot attach click handler');
}
// Initialize upcoming tours editor
function initializeUpcomingTours() {
console.log('=== Initializing upcoming tours editor ===');
console.log('upcomingEditor:', upcomingEditor);
console.log('upcomingTextarea:', upcomingTextarea);
console.log('addUpcomingBtn:', addUpcomingBtn);
if (!upcomingEditor) {
console.error('upcomingToursEditor element not found!');
return;
}
if (!upcomingTextarea) {
console.error('campDetailSidebarUpcomingTours textarea not found!');
return;
}
if (!addUpcomingBtn) {
console.error('addUpcomingTourBtn button not found!');
return;
}
console.log('Textarea value:', upcomingTextarea.value);
try {
const initialUpcoming = parseUpcomingTours();
console.log('Initial upcoming tours loaded:', initialUpcoming);
renderUpcomingTours(initialUpcoming);
console.log('Upcoming tours editor initialized successfully');
} catch (e) {
console.error('Error parsing/rendering upcoming tours:', e);
renderUpcomingTours([]);
}
}
// Call initialization with delay to ensure accordion is rendered
setTimeout(() => {
initializeUpcomingTours();
}, 100);
// Ensure upcoming tours sync on submit
if (form) {
form.addEventListener('submit', function () {
try { upcomingTextarea && (upcomingTextarea.value = upcomingTextarea.value); } catch (e) {}
});
}
// Update tips preview when tips image path changes
if (tipsImageInput) {
tipsImageInput.addEventListener('change', function () {
if (this.value) {
let src = this.value;
if (!src.startsWith('http') && !src.startsWith('/')) {
src = '/uploads/' + src;
}
if (src.startsWith('/')) {
src = window.location.origin + src;
}
tipsPreviewImg.src = src;
tipsImagePreview.classList.remove('d-none');
} else {
tipsImagePreview.classList.add('d-none');
}
});
}
// Hero background image preview: update on load and when input changes
const heroBgInput = document.getElementById('campDetailHeroBgImage');
const heroPreviewImg = document.getElementById('campDetailHeroBgPreview');
const heroPreviewWrapper = document.getElementById('campDetailHeroBgPreviewWrapper');
function updateHeroPreview() {
if (!heroBgInput) return;
const val = (heroBgInput.value || '').trim();
if (!val) {
if (heroPreviewWrapper) heroPreviewWrapper.style.display = 'none';
return;
}
let src = val;
// If not an absolute URL or data URL, prefix with origin for relative paths
if (!/^https?:\/\//i.test(src) && !/^data:/i.test(src)) {
if (src.startsWith('/')) src = window.location.origin + src;
else src = window.location.origin + '/' + src;
}
if (heroPreviewImg) {
heroPreviewImg.src = src;
if (heroPreviewWrapper) heroPreviewWrapper.style.display = 'block';
}
}
if (heroBgInput) {
heroBgInput.addEventListener('input', updateHeroPreview);
// Initialize preview on load
updateHeroPreview();
}
// Hero banner image preview and upload
const heroBannerInput = document.getElementById('heroBannerImage');
const heroBannerPreviewImg = document.getElementById('heroBannerPreview');
const heroBannerPreviewWrapper = document.getElementById('heroBannerPreviewWrapper');
function updateHeroBannerPreview() {
if (!heroBannerInput) return;
const val = (heroBannerInput.value || '').trim();
if (!val) {
if (heroBannerPreviewWrapper) heroBannerPreviewWrapper.style.display = 'none';
return;
}
let src = val;
if (!src.startsWith('http') && !src.startsWith('data:') && !src.startsWith('/')) {
src = '/uploads/' + src;
}
if (src.startsWith('/')) {
src = window.location.origin + src;
}
if (heroBannerPreviewImg) heroBannerPreviewImg.src = src;
if (heroBannerPreviewWrapper) heroBannerPreviewWrapper.style.display = 'block';
}
if (heroBannerInput) {
heroBannerInput.addEventListener('input', updateHeroBannerPreview);
updateHeroBannerPreview();
}
// Hero banner upload button
const uploadHeroBannerBtn = document.getElementById('uploadHeroBannerBtn');
if (uploadHeroBannerBtn) {
uploadHeroBannerBtn.addEventListener('click', function () {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
uploadHeroBannerBtn.disabled = true;
uploadHeroBannerBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
try {
const response = await fetch('/admin/upload/image?imageType=activity', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
heroBannerInput.value = result.path;
updateHeroBannerPreview();
showToast('Success', 'Hero banner uploaded', 'success');
} else {
showToast('Error', result.message || 'Upload failed', 'error');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', 'Upload failed', 'error');
} finally {
uploadHeroBannerBtn.disabled = false;
uploadHeroBannerBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
}
};
fileInput.click();
});
}
// Hero background upload button
const uploadHeroBtn = document.getElementById('uploadHeroBgBtn');
if (uploadHeroBtn) {
uploadHeroBtn.addEventListener('click', function () {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
uploadHeroBtn.disabled = true;
uploadHeroBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
try {
const response = await fetch('/admin/upload/image?imageType=activity', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
heroBgInput.value = result.path;
const previewUrl = result.url || (window.location.origin + result.path);
if (heroPreviewImg) heroPreviewImg.src = previewUrl;
if (heroPreviewWrapper) heroPreviewWrapper.style.display = 'block';
showToast('Success', 'Hero image uploaded', 'success');
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', error.message || 'Upload failed', 'error');
} finally {
uploadHeroBtn.disabled = false;
uploadHeroBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
}
};
fileInput.click();
});
}
// (removed image input preview — Tips image sidebar is used)
// Auto-generate link from name
const nameInput = document.querySelector('input[name="name"]');
const linkInput = document.querySelector('input[name="link"]');
if (nameInput && linkInput) {
nameInput.addEventListener('input', function () {
if (!linkInput.value || linkInput.dataset.autoGenerated === 'true') {
const slug = this.value
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
.trim();
linkInput.value = slug;
linkInput.dataset.autoGenerated = 'true';
}
});
linkInput.addEventListener('input', function () {
this.dataset.autoGenerated = 'false';
// Clean up manual input
this.value = this.value
.toLowerCase()
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
});
}
// Form validation
if (form) {
form.addEventListener('submit', function(e) {
// Ensure isActiveHidden reflects the checkbox state so server gets current status
try {
const isActiveCheckbox = document.getElementById('isActive');
const isActiveHidden = document.getElementById('isActiveHidden');
if (isActiveHidden) isActiveHidden.value = (isActiveCheckbox && isActiveCheckbox.checked) ? 'true' : 'false';
} catch (err) { console.warn('isActive sync error', err); }
const nameValue = nameInput ? nameInput.value.trim() : '';
const priceInput = document.querySelector('input[name="price"]');
const priceValue = priceInput ? priceInput.value : '';
if (!nameValue) {
e.preventDefault();
showToast('Validation Error', 'Activity name is required', 'error');
if (nameInput) nameInput.focus();
return false;
}
if (!priceValue || parseFloat(priceValue) < 0) {
e.preventDefault();
showToast('Validation Error', 'Valid price is required', 'error');
if (priceInput) priceInput.focus();
return false;
}
// Validate age range
const ageMinInput = document.querySelector('input[name="ageMin"]');
const ageMaxInput = document.querySelector('input[name="ageMax"]');
const ageMin = ageMinInput ? parseInt(ageMinInput.value) || 0 : 0;
const ageMax = ageMaxInput ? parseInt(ageMaxInput.value) || 0 : 99;
if (ageMin >= ageMax) {
e.preventDefault();
showToast('Validation Error', 'Maximum age must be greater than minimum age', 'error');
if (ageMaxInput) ageMaxInput.focus();
return false;
}
});
}
});
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());
}
// Sections Content Management Functions
let sectionsData = {};
// Initialize sections data from existing JSON
function initializeSectionsData() {
const sectionsTextarea = document.getElementById('sectionsJson');
if (!sectionsTextarea) {
console.error('Sections JSON textarea not found');
return;
}
try {
const existingData = sectionsTextarea.value ? JSON.parse(sectionsTextarea.value) : {};
sectionsData = existingData;
console.log('Initialized sections data:', sectionsData);
} catch (e) {
console.warn('Error parsing sections JSON:', e);
sectionsData = {};
}
// Initialize all section editors
initializeOverviewEditor();
initializeLocationEditor();
initializeAccommodationEditor();
initializeProgramEditor();
initializeMealsEditor();
initializeTeamEditor();
initializeInsuranceEditor();
// Set up real-time synchronization for all inputs
setupSectionsSync();
// Initialize previews for section image inputs
try { if (typeof initializeSectionImagePreviews === 'function') initializeSectionImagePreviews(); } catch (e) { console.warn('initializeSectionImagePreviews error', e); }
}
// Sync sections data to hidden textarea
function syncSectionsData() {
const sectionsTextarea = document.getElementById('sectionsJson');
if (sectionsTextarea) {
sectionsTextarea.value = JSON.stringify(sectionsData, null, 2);
console.log('Synced sections data to textarea');
}
}
// Set up real-time synchronization for section inputs
function setupSectionsSync() {
const sectionInputs = document.querySelectorAll('[data-section]');
sectionInputs.forEach(input => {
input.addEventListener('input', function() {
const section = this.getAttribute('data-section');
const field = this.getAttribute('data-field');
if (!sectionsData[section]) {
sectionsData[section] = {};
}
// Handle nested fields like "package.title"
if (field.includes('.')) {
const fieldParts = field.split('.');
let current = sectionsData[section];
for (let i = 0; i < fieldParts.length - 1; i++) {
if (!current[fieldParts[i]]) {
current[fieldParts[i]] = {};
}
current = current[fieldParts[i]];
}
current[fieldParts[fieldParts.length - 1]] = this.value;
} else {
sectionsData[section][field] = this.value;
}
syncSectionsData();
});
});
}
// Add feature function for overview section
function addFeature(section) {
const listContainer = document.getElementById(`${section}FeaturesList`);
if (!listContainer) return;
if (!sectionsData[section]) sectionsData[section] = {};
if (!sectionsData[section].features) sectionsData[section].features = [];
sectionsData[section].features.push('');
renderFeaturesList(section);
syncSectionsData();
}
// Render features list
function renderFeaturesList(section) {
const listContainer = document.getElementById(`${section}FeaturesList`);
if (!listContainer) return;
const features = (sectionsData[section] && sectionsData[section].features) || [];
listContainer.innerHTML = '';
features.forEach((feature, index) => {
const featureRow = document.createElement('div');
featureRow.className = 'input-group mb-2';
featureRow.innerHTML = `
<span class="input-group-text">${index + 1}</span>
<input type="text" class="form-control" value="${feature}" data-feature-index="${index}">
<button class="btn btn-outline-danger" type="button" onclick="removeFeature('${section}', ${index})">
<i class="fas fa-trash"></i>
</button>
`;
listContainer.appendChild(featureRow);
// Add event listener for feature input
const input = featureRow.querySelector('input');
input.addEventListener('input', function() {
if (!sectionsData[section]) sectionsData[section] = {};
if (!sectionsData[section].features) sectionsData[section].features = [];
sectionsData[section].features[index] = this.value;
syncSectionsData();
});
});
}
// Remove feature
function removeFeature(section, index) {
if (sectionsData[section] && sectionsData[section].features) {
sectionsData[section].features.splice(index, 1);
renderFeaturesList(section);
syncSectionsData();
}
}
// Add location image
function addLocationImage() {
if (!sectionsData.location) sectionsData.location = {};
if (!sectionsData.location.images) sectionsData.location.images = [];
sectionsData.location.images.push('');
renderLocationImages();
syncSectionsData();
}
// Render location images
function renderLocationImages() {
const listContainer = document.getElementById('locationImagesList');
if (!listContainer) return;
const images = (sectionsData.location && sectionsData.location.images) || [];
listContainer.innerHTML = '';
images.forEach((image, index) => {
const imageRow = document.createElement('div');
imageRow.className = 'd-flex align-items-center gap-2 mb-2';
// Preview thumbnail
const thumb = document.createElement('img');
thumb.className = 'rounded';
thumb.style.width = '100px';
thumb.style.height = '70px';
thumb.style.objectFit = 'cover';
thumb.alt = 'Location image';
// Input wrapper
const inputWrapper = document.createElement('div');
inputWrapper.className = 'flex-grow-1';
const input = document.createElement('input');
input.type = 'text';
input.className = 'form-control';
input.placeholder = 'Image URL or path';
input.value = image || '';
inputWrapper.appendChild(input);
// Upload button (reuse selectImageForLocation)
const uploadBtn = document.createElement('button');
uploadBtn.type = 'button';
uploadBtn.className = 'btn btn-outline-secondary';
uploadBtn.innerHTML = '<i class="fas fa-upload"></i>';
uploadBtn.addEventListener('click', () => selectImageForLocation(index));
// Remove button
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'btn btn-outline-danger';
removeBtn.innerHTML = '<i class="fas fa-trash"></i>';
removeBtn.addEventListener('click', () => removeLocationImage(index));
// set initial preview src
(function setInitial() {
const v = (image || '').trim();
if (!v) {
thumb.src = '/images/placeholder.png';
return;
}
let s = v;
if (!/^https?:\/\//i.test(s) && !/^data:/i.test(s)) {
s = s.startsWith('/') ? s : ('/uploads/' + s);
}
thumb.src = s;
})();
// update sectionsData & preview when input changes
input.addEventListener('input', function () {
if (!sectionsData.location) sectionsData.location = {};
if (!sectionsData.location.images) sectionsData.location.images = [];
sectionsData.location.images[index] = this.value;
syncSectionsData();
const v = (this.value || '').trim();
if (!v) { thumb.src = '/images/placeholder.png'; return; }
let s = v;
if (!/^https?:\/\//i.test(s) && !/^data:/i.test(s)) {
s = s.startsWith('/') ? s : ('/uploads/' + s);
}
thumb.src = s;
});
imageRow.appendChild(thumb);
imageRow.appendChild(inputWrapper);
imageRow.appendChild(uploadBtn);
imageRow.appendChild(removeBtn);
listContainer.appendChild(imageRow);
});
}
// Remove location image
function removeLocationImage(index) {
if (sectionsData.location && sectionsData.location.images) {
sectionsData.location.images.splice(index, 1);
renderLocationImages();
syncSectionsData();
}
}
// Select image for location (can be extended to open file picker)
function selectImageForLocation(index) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('/admin/upload/image?imageType=activity', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
if (!sectionsData.location) sectionsData.location = {};
if (!sectionsData.location.images) sectionsData.location.images = [];
sectionsData.location.images[index] = result.path;
renderLocationImages();
syncSectionsData();
showToast('Success', 'Location image uploaded', 'success');
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', error.message || 'Upload failed', 'error');
}
};
fileInput.click();
}
// Add text item (for intro, outro, details, principles, footer)
function addTextItem(section, type) {
if (!sectionsData[section]) sectionsData[section] = {};
const fieldName = type === 'intro' ? 'introText' :
type === 'outro' ? 'outroText' :
type === 'footer' ? 'footerText' :
type;
if (!sectionsData[section][fieldName]) sectionsData[section][fieldName] = [];
sectionsData[section][fieldName].push('');
renderTextItems(section, type);
syncSectionsData();
}
// Render text items
function renderTextItems(section, type) {
const listContainer = document.getElementById(`${section}${type.charAt(0).toUpperCase() + type.slice(1)}List`);
if (!listContainer) return;
const fieldName = type === 'intro' ? 'introText' :
type === 'outro' ? 'outroText' :
type === 'footer' ? 'footerText' :
type;
const items = (sectionsData[section] && sectionsData[section][fieldName]) || [];
listContainer.innerHTML = '';
items.forEach((item, index) => {
const itemRow = document.createElement('div');
itemRow.className = 'input-group mb-2';
itemRow.innerHTML = `
<span class="input-group-text">${index + 1}</span>
<textarea class="form-control" rows="2" placeholder="${type} text...">${item}</textarea>
<button class="btn btn-outline-danger" type="button" onclick="removeTextItem('${section}', '${type}', ${index})">
<i class="fas fa-trash"></i>
</button>
`;
listContainer.appendChild(itemRow);
// Add event listener for textarea
const textarea = itemRow.querySelector('textarea');
textarea.addEventListener('input', function() {
if (!sectionsData[section]) sectionsData[section] = {};
if (!sectionsData[section][fieldName]) sectionsData[section][fieldName] = [];
sectionsData[section][fieldName][index] = this.value;
syncSectionsData();
});
});
}
// Remove text item
function removeTextItem(section, type, index) {
const fieldName = type === 'intro' ? 'introText' :
type === 'outro' ? 'outroText' :
type === 'footer' ? 'footerText' :
type;
if (sectionsData[section] && sectionsData[section][fieldName]) {
sectionsData[section][fieldName].splice(index, 1);
renderTextItems(section, type);
syncSectionsData();
}
}
// Add meal item
function addMealItem() {
if (!sectionsData.meals) sectionsData.meals = {};
if (!sectionsData.meals.items) sectionsData.meals.items = [];
sectionsData.meals.items.push({ title: '', desc: '' });
renderMealItems();
syncSectionsData();
}
// Render meal items
function renderMealItems() {
const listContainer = document.getElementById('mealsItemsList');
if (!listContainer) return;
const items = (sectionsData.meals && sectionsData.meals.items) || [];
listContainer.innerHTML = '';
items.forEach((item, index) => {
const itemRow = document.createElement('div');
itemRow.className = 'card card-body mb-2';
itemRow.innerHTML = `
<div class="row g-2">
<div class="col-md-4">
<label class="form-label fw-medium">Meal Title</label>
<input type="text" class="form-control" value="${item.title}" placeholder="e.g., Breakfast">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Description</label>
<textarea class="form-control" rows="2" placeholder="Meal description...">${item.desc}</textarea>
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-outline-danger btn-sm" type="button" onclick="removeMealItem(${index})">
<i class="fas fa-trash"></i> Remove
</button>
</div>
</div>
`;
listContainer.appendChild(itemRow);
// Add event listeners
const titleInput = itemRow.querySelector('input');
const descTextarea = itemRow.querySelector('textarea');
titleInput.addEventListener('input', function() {
if (!sectionsData.meals) sectionsData.meals = {};
if (!sectionsData.meals.items) sectionsData.meals.items = [];
if (!sectionsData.meals.items[index]) sectionsData.meals.items[index] = {};
sectionsData.meals.items[index].title = this.value;
syncSectionsData();
});
descTextarea.addEventListener('input', function() {
if (!sectionsData.meals) sectionsData.meals = {};
if (!sectionsData.meals.items) sectionsData.meals.items = [];
if (!sectionsData.meals.items[index]) sectionsData.meals.items[index] = {};
sectionsData.meals.items[index].desc = this.value;
syncSectionsData();
});
});
}
// Remove meal item
function removeMealItem(index) {
if (sectionsData.meals && sectionsData.meals.items) {
sectionsData.meals.items.splice(index, 1);
renderMealItems();
syncSectionsData();
}
}
// Add insurance item
function addInsuranceItem(type) {
if (!sectionsData.insurance) sectionsData.insurance = {};
if (!sectionsData.insurance[type]) sectionsData.insurance[type] = {};
if (!sectionsData.insurance[type].items) sectionsData.insurance[type].items = [];
sectionsData.insurance[type].items.push('');
renderInsuranceItems(type);
syncSectionsData();
}
// Render insurance items
function renderInsuranceItems(type) {
const listContainer = document.getElementById(`insurance${type.charAt(0).toUpperCase() + type.slice(1)}ItemsList`);
if (!listContainer) return;
const items = (sectionsData.insurance && sectionsData.insurance[type] && sectionsData.insurance[type].items) || [];
listContainer.innerHTML = '';
items.forEach((item, index) => {
const itemRow = document.createElement('div');
itemRow.className = 'input-group mb-2';
itemRow.innerHTML = `
<span class="input-group-text">${index + 1}</span>
<input type="text" class="form-control" value="${item}" placeholder="Insurance item...">
<button class="btn btn-outline-danger" type="button" onclick="removeInsuranceItem('${type}', ${index})">
<i class="fas fa-trash"></i>
</button>
`;
listContainer.appendChild(itemRow);
// Add event listener
const input = itemRow.querySelector('input');
input.addEventListener('input', function() {
if (!sectionsData.insurance) sectionsData.insurance = {};
if (!sectionsData.insurance[type]) sectionsData.insurance[type] = {};
if (!sectionsData.insurance[type].items) sectionsData.insurance[type].items = [];
sectionsData.insurance[type].items[index] = this.value;
syncSectionsData();
});
});
}
// Remove insurance item
function removeInsuranceItem(type, index) {
if (sectionsData.insurance && sectionsData.insurance[type] && sectionsData.insurance[type].items) {
sectionsData.insurance[type].items.splice(index, 1);
renderInsuranceItems(type);
syncSectionsData();
}
}
// Image selection helper function
function selectImage(button) {
const input = button.previousElementSibling;
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('image', file);
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch('/admin/upload/image?imageType=activity', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
input.value = result.path;
input.dispatchEvent(new Event('input'));
showToast('Success', 'Image uploaded', 'success');
} else {
throw new Error(result.error || 'Upload failed');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Error', error.message || 'Upload failed', 'error');
} finally {
button.disabled = false;
button.innerHTML = '<i class="fas fa-image"></i>';
}
};
fileInput.click();
}
// Initialize image previews for section image inputs (inputs with data-field containing 'image')
function initializeSectionImagePreviews() {
const imgInputs = document.querySelectorAll('input[data-field]');
imgInputs.forEach(inp => {
const field = inp.getAttribute('data-field') || '';
if (!/image/i.test(field)) return;
// attach preview if not exists
const container = inp.closest('.input-group') || inp.parentElement;
if (!container) return;
if (container.querySelector('.section-image-preview')) return;
const preview = document.createElement('img');
preview.className = 'section-image-preview img-fluid rounded mt-2';
preview.style.maxHeight = '200px';
preview.style.objectFit = 'cover';
preview.style.width = '100%';
// determine initial src
const val = (inp.value || '').trim();
let src = '/images/placeholder.png';
if (val) {
if (!/^https?:\/\//i.test(val) && !/^data:/i.test(val)) {
src = val.startsWith('/') ? val : ('/uploads/' + val);
} else src = val;
}
preview.src = src;
container.appendChild(preview);
// update preview when input changes
inp.addEventListener('input', () => {
const v = (inp.value || '').trim();
if (!v) {
preview.src = '/images/placeholder.png';
return;
}
let s = v;
if (!/^https?:\/\//i.test(s) && !/^data:/i.test(s)) {
s = s.startsWith('/') ? s : ('/uploads/' + s);
}
preview.src = s;
});
});
}
// Initialize individual section editors
function initializeOverviewEditor() {
renderFeaturesList('overview');
}
function initializeLocationEditor() {
renderLocationImages();
}
function initializeAccommodationEditor() {
renderTextItems('accommodation', 'intro');
renderTextItems('accommodation', 'outro');
renderTextItems('accommodation', 'details');
renderTextItems('accommodation', 'principles');
}
function initializeProgramEditor() {
renderTextItems('program', 'intro');
renderTextItems('program', 'outro');
renderTextItems('program', 'principles');
renderTextItems('program', 'footer');
}
function initializeMealsEditor() {
renderMealItems();
}
function initializeTeamEditor() {
renderTextItems('team', 'intro');
renderTextItems('team', 'footer');
}
function initializeInsuranceEditor() {
renderInsuranceItems('package');
renderInsuranceItems('cancellation');
}
// Initialize sections editor on DOM ready
document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
initializeSectionsData();
}, 200);
});
// ==================== BOOKING SESSIONS ====================
let bookingSessions = [];
function initBookingSessions() {
try {
const jsonEl = document.getElementById('bookingSessionsJson');
bookingSessions = JSON.parse(jsonEl.value || '[]');
} catch (e) {
bookingSessions = [];
}
renderBookingSessions();
}
function renderBookingSessions() {
const container = document.getElementById('bookingSessionsList');
const noMsg = document.getElementById('noSessionsMessage');
if (!bookingSessions || bookingSessions.length === 0) {
container.innerHTML = '';
noMsg.style.display = 'block';
return;
}
noMsg.style.display = 'none';
container.innerHTML = bookingSessions.map((session, index) => {
const startDate = session.startDate ? new Date(session.startDate).toISOString().split('T')[0] : '';
const endDate = session.endDate ? new Date(session.endDate).toISOString().split('T')[0] : '';
const freeMale = (session.totalMaleSpots || 0) - (session.bookedMaleSpots || 0);
const freeFemale = (session.totalFemaleSpots || 0) - (session.bookedFemaleSpots || 0);
const freeTotal = freeMale + freeFemale;
const statusBadge = session.isActive
? '<span class="badge bg-success">Active</span>'
: '<span class="badge bg-secondary">Inactive</span>';
const fullBadge = freeTotal <= 0
? '<span class="badge bg-danger ms-1">FULL</span>'
: '';
return `
<div class="card mb-3 border" data-session-index="${index}" data-session-id="${session.sessionId || 'session-' + index}" id="booking-session-${session.sessionId || 'session-' + index}">
<div class="card-header bg-light d-flex justify-content-between align-items-center py-2">
<div>
<strong>Session ${index + 1}</strong>
${statusBadge} ${fullBadge}
<small class="text-muted ms-2">(ID: ${session.sessionId || 'session-' + index})</small>
</div>
<div>
<button type="button" class="btn btn-sm btn-outline-primary me-1" onclick="editBookingSession(${index})">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteBookingSession(${index})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="card-body py-2">
<div class="row g-2 small">
<div class="col-md-3">
<i class="fas fa-calendar me-1 text-primary"></i>
<strong>Date:</strong> ${startDate} → ${endDate}
</div>
<div class="col-md-2">
<i class="fas fa-moon me-1 text-info"></i>
<strong>Nights:</strong> ${session.overnightStays || 0}
</div>
<div class="col-md-2">
<i class="fas fa-mars me-1 text-primary"></i>
<strong>Male:</strong> ${freeMale}/${session.totalMaleSpots || 0}
</div>
<div class="col-md-2">
<i class="fas fa-venus me-1 text-danger"></i>
<strong>Female:</strong> ${freeFemale}/${session.totalFemaleSpots || 0}
</div>
<div class="col-md-3">
<i class="fas fa-dollar-sign me-1 text-warning"></i>
<strong>Price:</strong> $${session.price || 'Default'}
</div>
</div>
</div>
</div>
`;
}).join('');
}
function addBookingSession() {
const modal = createSessionModal();
modal.show();
}
function editBookingSession(index) {
const session = bookingSessions[index];
const modal = createSessionModal(session, index);
modal.show();
}
function deleteBookingSession(index) {
if (confirm('Are you sure you want to delete this session?')) {
bookingSessions.splice(index, 1);
syncBookingSessionsJson();
renderBookingSessions();
}
}
function createSessionModal(session = null, editIndex = -1) {
const isEdit = editIndex >= 0;
const modalId = 'bookingSessionModal';
// Remove existing modal if any
const existingModal = document.getElementById(modalId);
if (existingModal) existingModal.remove();
const startDate = session?.startDate ? new Date(session.startDate).toISOString().split('T')[0] : '';
const endDate = session?.endDate ? new Date(session.endDate).toISOString().split('T')[0] : '';
const modalHtml = `
<div class="modal fade" id="${modalId}" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${isEdit ? 'Edit' : 'Add'} Booking Session</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Start Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" id="sessionStartDate" value="${startDate}">
</div>
<div class="col-md-6">
<label class="form-label">End Date <span class="text-danger">*</span></label>
<input type="date" class="form-control" id="sessionEndDate" value="${endDate}">
</div>
<div class="col-md-6">
<label class="form-label">Overnight Stays <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="sessionOvernightStays"
value="${session?.overnightStays || 14}" min="1" required>
</div>
<div class="col-md-6">
<label class="form-label">Price (leave empty for default)</label>
<input type="number" class="form-control" id="sessionPrice"
value="${session?.price || ''}" min="0" step="0.01"
placeholder="e.g., 395">
</div>
<!-- Male Spots -->
<div class="col-md-6">
<label class="form-label">Male - Total Spots <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="sessionTotalMaleSpots"
value="${session?.totalMaleSpots || 25}" min="0" required>
</div>
<div class="col-md-6">
<label class="form-label">Male - Booked Spots</label>
<input type="number" class="form-control" id="sessionBookedMaleSpots"
value="${session?.bookedMaleSpots || 0}" min="0">
</div>
<!-- Female Spots -->
<div class="col-md-6">
<label class="form-label">Female - Total Spots <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="sessionTotalFemaleSpots"
value="${session?.totalFemaleSpots || 25}" min="0" required>
</div>
<div class="col-md-6">
<label class="form-label">Female - Booked Spots</label>
<input type="number" class="form-control" id="sessionBookedFemaleSpots"
value="${session?.bookedFemaleSpots || 0}" min="0">
</div>
<div class="col-12">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="sessionIsActive"
${session?.isActive !== false ? 'checked' : ''}>
<label class="form-check-label" for="sessionIsActive">Active (visible for booking)</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveBookingSession(${editIndex})">
<i class="fas fa-save me-1"></i>${isEdit ? 'Update' : 'Add'} Session
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
return new bootstrap.Modal(document.getElementById(modalId));
}
function saveBookingSession(editIndex) {
const startDate = document.getElementById('sessionStartDate').value;
const endDate = document.getElementById('sessionEndDate').value;
const overnightStays = parseInt(document.getElementById('sessionOvernightStays').value) || 14;
const totalMaleSpots = parseInt(document.getElementById('sessionTotalMaleSpots').value) || 25;
const bookedMaleSpots = parseInt(document.getElementById('sessionBookedMaleSpots').value) || 0;
const totalFemaleSpots = parseInt(document.getElementById('sessionTotalFemaleSpots').value) || 25;
const bookedFemaleSpots = parseInt(document.getElementById('sessionBookedFemaleSpots').value) || 0;
const price = document.getElementById('sessionPrice').value ? parseFloat(document.getElementById('sessionPrice').value) : null;
const isActive = document.getElementById('sessionIsActive').checked;
if (!startDate || !endDate) {
alert('Please fill in Start Date and End Date');
return;
}
// Generate sessionId automatically (keep existing if editing)
let sessionId;
if (editIndex >= 0 && bookingSessions[editIndex]?.sessionId) {
sessionId = bookingSessions[editIndex].sessionId;
} else {
sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
const sessionData = {
sessionId,
startDate,
endDate,
overnightStays,
totalMaleSpots,
bookedMaleSpots,
totalFemaleSpots,
bookedFemaleSpots,
price,
isActive
};
if (editIndex >= 0) {
bookingSessions[editIndex] = sessionData;
} else {
bookingSessions.push(sessionData);
}
syncBookingSessionsJson();
renderBookingSessions();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('bookingSessionModal'));
modal.hide();
}
function syncBookingSessionsJson() {
document.getElementById('bookingSessionsJson').value = JSON.stringify(bookingSessions);
}
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', function() {
initBookingSessions();
<% if (isEdit && data._id) { %>
// Load booking count on page load
loadBookingCount('<%= data._id %>');
<% } %>
});
// ==================== BOOKING SUBMISSIONS ====================
let bookingSubmissions = [];
let filteredBookings = [];
let currentPage = 1;
const itemsPerPage = 10;
async function loadBookingCount(activityId) {
try {
const response = await fetch(`/admin/activity/${activityId}/bookings/count`);
const data = await response.json();
const count = data.count || 0;
const btnElement = document.getElementById('bookingCountBtn');
if (btnElement) btnElement.textContent = count;
} catch (error) {
console.error('Error loading booking count:', error);
const btnElement = document.getElementById('bookingCountBtn');
if (btnElement) btnElement.textContent = '0';
}
}
async function loadBookingSubmissions(activityId) {
const section = document.getElementById('bookingSubmissionsSection');
const button = document.getElementById('viewBookingsBtn');
const isCurrentlyVisible = section.style.display !== 'none';
if (!isCurrentlyVisible) {
// Show section and load data
section.style.display = 'block';
button.innerHTML = '<i class="fas fa-eye-slash me-1"></i>Hide Bookings (<span id="bookingCountBtn">Loading...</span>)';
try {
const response = await fetch(`/admin/activity/${activityId}/bookings`);
const data = await response.json();
bookingSubmissions = data.bookings || [];
filteredBookings = [...bookingSubmissions];
updateBookingStats(data.stats || {});
updateSessionBreakdown(data.sessionBreakdown || {});
populateSessionFilter(data.sessions || []);
renderBookingTable();
const count = data.stats?.total || 0;
button.innerHTML = `<i class="fas fa-eye-slash me-1"></i>Hide Bookings (<span id="bookingCountBtn">${count}</span>)`;
} catch (error) {
console.error('Error loading booking submissions:', error);
showBookingError('Failed to load booking submissions');
const prevCount = document.getElementById('bookingCountBtn')?.textContent || '0';
button.innerHTML = `<i class="fas fa-eye-slash me-1"></i>Hide Bookings (<span id="bookingCountBtn">${prevCount}</span>)`;
}
} else {
// Hide section
section.style.display = 'none';
const currentCount = document.getElementById('bookingCountBtn')?.textContent || '0';
button.innerHTML = `<i class="fas fa-eye me-1"></i>View Bookings (<span id="bookingCountBtn">${currentCount}</span>)`;
}
}
function updateBookingStats(stats) {
document.getElementById('totalBookings').textContent = stats.total || 0;
document.getElementById('confirmedBookings').textContent = stats.confirmed || 0;
document.getElementById('pendingBookings').textContent = stats.pending || 0;
document.getElementById('totalRevenue').textContent = '$' + (stats.totalRevenue || 0).toLocaleString();
}
function updateSessionBreakdown(breakdown) {
const container = document.getElementById('sessionBreakdownContent');
if (!breakdown || Object.keys(breakdown).length === 0) {
container.innerHTML = '<p class="text-muted">No sessions with bookings found.</p>';
return;
}
let html = '';
for (const [sessionId, data] of Object.entries(breakdown)) {
const capacity = data.totalCapacity || 0;
const booked = data.bookedCount || 0;
const percentage = capacity > 0 ? Math.round((booked / capacity) * 100) : 0;
html += `
<div class="card mb-2">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>${data.sessionName || sessionId}</strong>
<small class="text-muted d-block">${data.dateRange || 'Date not specified'}</small>
</div>
<div class="text-end">
<span class="badge bg-${percentage >= 80 ? 'warning' : percentage >= 60 ? 'info' : 'success'}">${booked}/${capacity}</span>
<div class="progress mt-1" style="width: 100px; height: 6px;">
<div class="progress-bar bg-${percentage >= 80 ? 'warning' : percentage >= 60 ? 'info' : 'success'}"
style="width: ${percentage}%"></div>
</div>
</div>
</div>
</div>
</div>
`;
}
container.innerHTML = html;
}
function populateSessionFilter(sessions) {
const select = document.getElementById('sessionFilter');
select.innerHTML = '<option value="">All Sessions</option>';
sessions.forEach(session => {
select.innerHTML += `<option value="${session.sessionId}">${session.sessionName || session.sessionId}</option>`;
});
}
function filterBookings() {
const statusFilter = document.getElementById('statusFilter').value;
const sessionFilter = document.getElementById('sessionFilter').value;
const paymentFilter = document.getElementById('paymentFilter').value;
const searchFilter = document.getElementById('searchFilter').value.toLowerCase();
filteredBookings = bookingSubmissions.filter(booking => {
if (statusFilter && booking.status !== statusFilter) return false;
if (sessionFilter && booking.sessionId !== sessionFilter) return false;
if (paymentFilter && booking.paymentStatus !== paymentFilter) return false;
if (searchFilter) {
const searchableText = `${booking.participantFirstName} ${booking.participantLastName} ${booking.parentFirstName} ${booking.parentLastName} ${booking.email}`.toLowerCase();
if (!searchableText.includes(searchFilter)) return false;
}
return true;
});
currentPage = 1;
renderBookingTable();
}
function renderBookingTable() {
const tbody = document.getElementById('bookingTableBody');
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const pageBookings = filteredBookings.slice(startIndex, endIndex);
if (pageBookings.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center py-4 text-muted">No bookings found</td></tr>';
document.getElementById('bookingPagination').style.display = 'none';
return;
}
tbody.innerHTML = pageBookings.map(booking => {
const createdDate = new Date(booking.createdAt).toLocaleDateString();
const participantAge = calculateAge(booking.participantBirthDate);
const statusBadge = getStatusBadge(booking.status);
const paymentBadge = getPaymentBadge(booking.paymentStatus);
return `
<tr>
<td><small>${createdDate}</small></td>
<td><small class="text-muted">${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()}</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>
<small class="d-block">${participantAge}y</small>
</td>
<td>${statusBadge}</td>
<td>${paymentBadge}</td>
<td>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="viewBookingDetails('${booking._id}')" title="View Details">
<i class="fas fa-eye"></i>
</button>
<button type="button" class="btn btn-outline-warning btn-sm" onclick="editBooking('${booking._id}')" title="Edit">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteBooking('${booking._id}')" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
renderPagination();
}
function renderPagination() {
const totalPages = Math.ceil(filteredBookings.length / itemsPerPage);
const paginationContainer = document.getElementById('bookingPagination');
const paginationList = document.getElementById('paginationList');
if (totalPages <= 1) {
paginationContainer.style.display = 'none';
return;
}
paginationContainer.style.display = 'block';
let paginationHTML = '';
// Previous button
paginationHTML += `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">Previous</a>
</li>
`;
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
paginationHTML += `
<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>
</li>
`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
paginationHTML += '<li class="page-item disabled"><span class="page-link">...</span></li>';
}
}
// Next button
paginationHTML += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">Next</a>
</li>
`;
paginationList.innerHTML = paginationHTML;
}
function changePage(page) {
if (page < 1 || page > Math.ceil(filteredBookings.length / itemsPerPage)) return;
currentPage = page;
renderBookingTable();
}
function calculateAge(birthDate) {
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;
}
function 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>';
}
function 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>';
}
function showBookingError(message) {
const tbody = document.getElementById('bookingTableBody');
tbody.innerHTML = `<tr><td colspan="9" class="text-center py-4 text-danger">${message}</td></tr>`;
}
async function refreshBookingData(activityId) {
await loadBookingSubmissions(activityId);
}
async function exportBookingData(activityId) {
try {
const response = await fetch(`/admin/activity/${activityId}/bookings/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `bookings_${activityId}_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Error exporting booking data:', error);
alert('Failed to export booking data');
}
}
function viewBookingDetails(bookingId) {
// Find booking in the filtered bookings array
const booking = filteredBookings.find(b => b._id === bookingId);
if (!booking) {
showBookingResultModal('error', 'Error', 'Booking not found');
return;
}
// Move modal to body if not already there
const modalEl = document.getElementById('bookingDetailsModal');
if (modalEl && modalEl.parentElement !== document.body) {
document.body.appendChild(modalEl);
}
// Format dates
const formatDate = (date) => {
if (!date) return 'Not specified';
return new Date(date).toLocaleDateString('en-GB', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const formatDateTime = (date) => {
if (!date) return 'Not specified';
return new Date(date).toLocaleString('en-GB', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Calculate age
const calculateDisplayAge = (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';
};
// Generate detailed content
const content = `
<div class="container-fluid">
<div class="row">
<!-- Left Column: Participant & Parent Info -->
<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 Information</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-12">
<strong class="text-primary fs-5">${booking.participantFirstName} ${booking.participantLastName}</strong>
</div>
<div class="col-6">
<small class="text-muted">Birth Date:</small><br>
<span>${formatDate(booking.participantBirthDate)}</span>
</div>
<div class="col-6">
<small class="text-muted">Age:</small><br>
<span>${calculateDisplayAge(booking.participantBirthDate)}</span>
</div>
<div class="col-6">
<small class="text-muted">Gender:</small><br>
<span class="badge bg-${booking.participantGender === 'male' ? 'primary' : booking.participantGender === 'female' ? 'danger' : 'secondary'}">
${booking.participantGender || 'N/A'}
</span>
</div>
<div class="col-6">
<small class="text-muted">Participants:</small><br>
<span class="badge bg-info">${booking.numberOfParticipants || 1} person(s)</span>
</div>
</div>
</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">
<div class="row g-2">
<div class="col-12">
<strong class="text-success">${booking.parentFirstName} ${booking.parentLastName}</strong>
</div>
<div class="col-12">
<small class="text-muted">Email:</small><br>
<a href="mailto:${booking.email}">${booking.email}</a>
</div>
<div class="col-12">
<small class="text-muted">Phone:</small><br>
<a href="tel:${booking.phone}">${booking.phone}</a>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Address & Emergency Contact -->
<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 Information</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-12">
<small class="text-muted">Address:</small><br>
<span>${booking.address || 'Not provided'}</span>
</div>
<div class="col-6">
<small class="text-muted">City:</small><br>
<span>${booking.city || 'Not provided'}</span>
</div>
<div class="col-6">
<small class="text-muted">Postal Code:</small><br>
<span>${booking.postalCode || 'Not provided'}</span>
</div>
<div class="col-12">
<small class="text-muted">Country:</small><br>
<span class="badge bg-secondary">${booking.country || 'Not provided'}</span>
</div>
</div>
</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">
<div class="row g-2">
<div class="col-12">
<small class="text-muted">Emergency Contact:</small><br>
<strong>${booking.emergencyContact || 'Not provided'}</strong>
</div>
<div class="col-12">
<small class="text-muted">Emergency Phone:</small><br>
<a href="tel:${booking.emergencyPhone || ''}">${booking.emergencyPhone || 'Not provided'}</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Medical & Special Information -->
<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>Medical & Dietary Information</h6>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<small class="text-muted">Dietary Restrictions:</small><br>
<div class="bg-light p-2 rounded">${booking.dietaryRestrictions || 'None'}</div>
</div>
<div class="col-md-6">
<small class="text-muted">Session:</small><br>
<code class="text-primary">${booking.sessionId || 'Not specified'}</code>
</div>
<div class="col-12">
<small class="text-muted">Medical Conditions:</small><br>
<div class="bg-light p-2 rounded">${booking.medicalConditions || 'None specified'}</div>
</div>
<div class="col-12">
<small class="text-muted">Special Requests:</small><br>
<div class="bg-light p-2 rounded">${booking.specialRequests || 'None specified'}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Booking Status & Payment -->
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<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">
<div class="row g-2">
<div class="col-6">
<small class="text-muted">Status:</small><br>
${getStatusBadge(booking.status || 'pending')}
</div>
<div class="col-6">
<small class="text-muted">Payment:</small><br>
${getPaymentBadge(booking.paymentStatus || 'pending')}
</div>
<div class="col-6">
<small class="text-muted">Total Amount:</small><br>
<strong class="text-success">$${booking.totalAmount || 0}</strong>
</div>
<div class="col-6">
<small class="text-muted">Paid Amount:</small><br>
<strong class="text-info">$${booking.paidAmount || 0}</strong>
</div>
${booking.confirmationCode ? `
<div class="col-12">
<small class="text-muted">Confirmation Code:</small><br>
<code class="bg-warning px-2 py-1 rounded">${booking.confirmationCode}</code>
</div>
` : ''}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header bg-dark text-white py-2">
<h6 class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Booking Information</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-12">
<small class="text-muted">Booking Date:</small><br>
<span>${formatDateTime(booking.createdAt)}</span>
</div>
<div class="col-6">
<small class="text-muted">Terms Agreed:</small><br>
<span class="badge ${booking.agreeTerms ? 'bg-success' : 'bg-danger'}">
${booking.agreeTerms ? 'Yes' : 'No'}
</span>
</div>
<div class="col-6">
<small class="text-muted">Newsletter:</small><br>
<span class="badge ${booking.agreeNewsletter ? 'bg-success' : 'bg-secondary'}">
${booking.agreeNewsletter ? 'Yes' : 'No'}
</span>
</div>
${booking.adminNotes ? `
<div class="col-12">
<small class="text-muted">Admin Notes:</small><br>
<div class="bg-warning bg-opacity-10 p-2 rounded border border-warning">${booking.adminNotes}</div>
</div>
` : ''}
</div>
</div>
</div>
</div>
</div>
</div>
`;
// Update modal content and show
document.getElementById('bookingDetailsContent').innerHTML = content;
document.getElementById('editBookingFromDetailsBtn').setAttribute('data-booking-id', bookingId);
const modal = new bootstrap.Modal(document.getElementById('bookingDetailsModal'));
modal.show();
}
function editBooking(bookingId) {
// Find booking in the filtered bookings array
const booking = filteredBookings.find(b => b._id === bookingId);
if (!booking) {
showBookingResultModal('error', 'Error', 'Booking not found');
return;
}
// Move modal to body if not already there
const modalEl = document.getElementById('editBookingModal');
if (modalEl && modalEl.parentElement !== document.body) {
document.body.appendChild(modalEl);
}
// Generate edit form
const content = `
<form id="editBookingForm">
<input type="hidden" id="editBookingId" value="${booking._id}">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Status</label>
<select class="form-select" id="editBookingStatus">
<option value="pending" ${booking.status === 'pending' ? 'selected' : ''}>Pending</option>
<option value="confirmed" ${booking.status === 'confirmed' ? 'selected' : ''}>Confirmed</option>
<option value="cancelled" ${booking.status === 'cancelled' ? 'selected' : ''}>Cancelled</option>
<option value="completed" ${booking.status === 'completed' ? 'selected' : ''}>Completed</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Payment Status</label>
<select class="form-select" id="editPaymentStatus">
<option value="pending" ${booking.paymentStatus === 'pending' ? 'selected' : ''}>Pending</option>
<option value="partial" ${booking.paymentStatus === 'partial' ? 'selected' : ''}>Partial</option>
<option value="paid" ${booking.paymentStatus === 'paid' ? 'selected' : ''}>Paid</option>
<option value="refunded" ${booking.paymentStatus === 'refunded' ? 'selected' : ''}>Refunded</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Total Amount ($)</label>
<input type="number" class="form-control" id="editTotalAmount" value="${booking.totalAmount || 0}" min="0" step="0.01">
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Paid Amount ($)</label>
<input type="number" class="form-control" id="editPaidAmount" value="${booking.paidAmount || 0}" min="0" step="0.01">
</div>
<div class="col-12">
<label class="form-label fw-medium">Admin Notes</label>
<textarea class="form-control" id="editAdminNotes" rows="3" placeholder="Add notes about this booking...">${booking.adminNotes || ''}</textarea>
</div>
</div>
</form>
`;
document.getElementById('editBookingContent').innerHTML = content;
document.getElementById('saveBookingBtn').setAttribute('data-booking-id', bookingId);
const modal = new bootstrap.Modal(document.getElementById('editBookingModal'));
modal.show();
}
// Wire up save booking button
document.addEventListener('DOMContentLoaded', function() {
// Auto-load booking count on page ready
<% if (isEdit && data._id) { %>
loadBookingCount('<%= data._id %>');
<% } %>
// Check for scrollTo parameter or #bookings hash in URL
(function handleScrollFromLink() {
const urlParams = new URLSearchParams(window.location.search);
const scrollToSession = urlParams.get('scrollTo');
const hash = window.location.hash || '';
// If session specified, auto-load bookings and scroll to session
if (scrollToSession) {
// Load bookings section automatically
const bookingsSection = document.getElementById('bookingSubmissionsSection');
if (bookingsSection && bookingsSection.style.display === 'none') {
loadBookingSubmissions('<%= data._id %>');
}
// Find and scroll to target session
const tryIds = [scrollToSession, `booking-session-${scrollToSession}`, `session-${scrollToSession}`];
let targetSession = null;
// Retry finding session after bookings load
const findAndScroll = (attempt = 0) => {
for (const id of tryIds) {
const el = document.getElementById(id);
if (el) { targetSession = el; break; }
}
if (targetSession) {
targetSession.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
// temporary highlight
const prevOutline = targetSession.style.outline;
targetSession.style.outline = '4px solid #ffc10766';
setTimeout(() => { targetSession.style.outline = prevOutline; }, 3000);
} else if (attempt < 3) {
setTimeout(() => findAndScroll(attempt + 1), 500);
}
};
setTimeout(findAndScroll, 800);
return;
}
// If hash #bookings present, auto-load and scroll to bookings
if (hash === '#bookings') {
const bookingsSection = document.getElementById('bookingSubmissionsSection');
if (bookingsSection && bookingsSection.style.display === 'none') {
loadBookingSubmissions('<%= data._id %>');
}
const anchor = document.getElementById('bookings');
if (anchor) {
setTimeout(() => anchor.scrollIntoView({ behavior: 'smooth', block: 'start' }), 600);
}
}
})();
// Edit from details modal button
const editFromDetailsBtn = document.getElementById('editBookingFromDetailsBtn');
if (editFromDetailsBtn) {
editFromDetailsBtn.addEventListener('click', function() {
const bookingId = this.getAttribute('data-booking-id');
// Close details modal
const detailsModal = bootstrap.Modal.getInstance(document.getElementById('bookingDetailsModal'));
if (detailsModal) detailsModal.hide();
// Open edit modal
setTimeout(() => editBooking(bookingId), 300);
});
}
// Save booking button
const saveBtn = document.getElementById('saveBookingBtn');
if (saveBtn) {
saveBtn.addEventListener('click', async function() {
const bookingId = this.getAttribute('data-booking-id');
const status = document.getElementById('editBookingStatus').value;
const paymentStatus = document.getElementById('editPaymentStatus').value;
const totalAmount = parseFloat(document.getElementById('editTotalAmount').value) || 0;
const paidAmount = parseFloat(document.getElementById('editPaidAmount').value) || 0;
const adminNotes = document.getElementById('editAdminNotes').value;
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
try {
const response = await fetch(`/admin/bookings/${bookingId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, paymentStatus, totalAmount, paidAmount, adminNotes })
});
if (response.ok) {
const modal = bootstrap.Modal.getInstance(document.getElementById('editBookingModal'));
if (modal) modal.hide();
showBookingResultModal('success', 'Success', 'Booking updated successfully');
// Refresh the booking list
await loadBookingSubmissions('<%= data._id %>');
} else {
const error = await response.json();
showBookingResultModal('error', 'Error', error.message || 'Failed to update booking');
}
} catch (error) {
console.error('Error updating booking:', error);
showBookingResultModal('error', 'Error', 'Network error: Failed to update booking');
} finally {
this.disabled = false;
this.innerHTML = '<i class="fas fa-save me-1"></i>Save Changes';
}
});
}
// Confirm delete booking button
const confirmDeleteBtn = document.getElementById('confirmDeleteBookingBtn');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', async function() {
const bookingId = this.getAttribute('data-booking-id');
this.disabled = true;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Deleting...';
try {
const response = await fetch(`/admin/bookings/${bookingId}`, {
method: 'DELETE'
});
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteBookingModal'));
if (deleteModal) deleteModal.hide();
if (response.ok) {
showBookingResultModal('success', 'Success', 'Booking deleted successfully');
// Refresh the booking list
await loadBookingSubmissions('<%= data._id %>');
} else {
const error = await response.json();
showBookingResultModal('error', 'Error', error.message || 'Failed to delete booking');
}
} catch (error) {
console.error('Error deleting booking:', error);
showBookingResultModal('error', 'Error', 'Network error: Failed to delete booking');
} finally {
this.disabled = false;
this.innerHTML = '<i class="fas fa-trash me-1"></i>Delete';
}
});
}
// Move modals to body to ensure they are on top of everything
['bookingDetailsModal', 'editBookingModal', 'deleteBookingModal', 'bookingResultModal'].forEach(id => {
const modal = document.getElementById(id);
if (modal && modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
});
});
async function deleteBooking(bookingId) {
// Find booking to get the name
const booking = filteredBookings.find(b => b._id === bookingId);
if (!booking) {
showBookingResultModal('error', 'Error', 'Booking not found');
return;
}
// Move modal to body if not already there
const modalEl = document.getElementById('deleteBookingModal');
if (modalEl && modalEl.parentElement !== document.body) {
document.body.appendChild(modalEl);
}
// Set the booking name in the modal
document.getElementById('deleteBookingName').textContent =
`${booking.participantFirstName} ${booking.participantLastName}`;
document.getElementById('confirmDeleteBookingBtn').setAttribute('data-booking-id', bookingId);
const modal = new bootstrap.Modal(document.getElementById('deleteBookingModal'));
modal.show();
}
function showBookingResultModal(type, title, message) {
const modal = document.getElementById('bookingResultModal');
// Move modal to body if not already there
if (modal && modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
// Remove previous classes
modal.classList.remove('success', 'error');
modal.classList.add(type);
// Update header based on type
const header = document.getElementById('bookingResultHeader');
if (type === 'success') {
header.className = 'modal-header bg-success text-white';
} else {
header.className = 'modal-header bg-danger text-white';
}
document.getElementById('bookingResultTitle').innerHTML =
`<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>${title}`;
document.getElementById('bookingResultMessage').textContent = message;
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}
</script>