forked from UKSOURCE/cms.hailearning.edu.vn
4156 lines
234 KiB
Plaintext
4156 lines
234 KiB
Plaintext
<div class="container">
|
||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||
<div>
|
||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||
<%= title %>
|
||
</h1>
|
||
<p class="text-muted mb-0">
|
||
<%= 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 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 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>
|