forked from UKSOURCE/cms.hailearning.edu.vn
2035 lines
114 KiB
Plaintext
2035 lines
114 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);">Booking Management</h1>
|
|
<p class="text-muted mb-0">Edit content displayed on Booking page</p>
|
|
</div>
|
|
<div>
|
|
<a href="<%= frontendUrl %>/booking/" class="btn btn-outline-primary" target="_blank">
|
|
<i class="fas fa-external-link-alt me-2"></i>View Booking Page
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<form method="POST" class="content-with-fixed-buttons" id="bookingForm"
|
|
action="<%= data && data._id ? ('/admin/booking/' + data._id + '/update') : '#' %>">
|
|
<!-- Hidden inputs for JSON data -->
|
|
<input type="hidden" name="hero" id="heroJson">
|
|
<input type="hidden" name="searchBar" id="searchBarJson">
|
|
<input type="hidden" name="filterPanel" id="filterPanelJson">
|
|
<input type="hidden" name="programs" id="programsJson">
|
|
<input type="hidden" name="holidays" id="holidaysJson">
|
|
<input type="hidden" name="locations" id="locationsJson">
|
|
<input type="hidden" name="camps" id="campsJson">
|
|
<input type="hidden" name="discounts" id="discountsJson">
|
|
<input type="hidden" name="vouchers" id="vouchersJson">
|
|
<input type="hidden" name="formSteps" id="formStepsJson">
|
|
<input type="hidden" name="validation" id="validationJson">
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-header bg-white border-bottom">
|
|
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
|
<li class="nav-item">
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
|
|
<i class="fas fa-home me-2"></i>Hero
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#searchBar" role="tab">
|
|
<i class="fas fa-search me-2"></i>Search Bar
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#filterPanel" role="tab">
|
|
<i class="fas fa-filter me-2"></i>Filter Panel
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#programs" role="tab">
|
|
<i class="fas fa-list me-2"></i>Programs
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#camps" role="tab">
|
|
<i class="fas fa-campground me-2"></i>Camps
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#discounts" role="tab">
|
|
<i class="fas fa-percent me-2"></i>Discounts
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#vouchers" role="tab">
|
|
<i class="fas fa-ticket-alt me-2"></i>Vouchers
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
|
|
<i class="fas fa-wpforms me-2"></i>Booking Form
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
<!-- Hero Tab -->
|
|
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-5">
|
|
<label class="form-label fw-medium">Background Image</label>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="heroBackgroundImage"
|
|
name="heroBackgroundImage"
|
|
value="<%= data.hero?.backgroundImage || '' %>">
|
|
<button type="button"
|
|
class="btn btn-outline-primary btn-upload-image"
|
|
data-target-input="heroBackgroundImage"
|
|
data-image-type="booking">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-7">
|
|
<% if (data.hero?.backgroundImage) { %>
|
|
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
|
|
style="height: 300px; width: 100%; object-fit: cover;"
|
|
alt="Background image preview">
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Title</label>
|
|
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
|
|
value="<%= data.hero?.title || '' %>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Bar Tab -->
|
|
<div class="tab-pane fade" id="searchBar" role="tabpanel">
|
|
<div class="card border shadow-sm mb-3">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-medium">Location Label</label>
|
|
<input type="text" class="form-control" id="searchBarLocationLabel"
|
|
value="<%= data.searchBar?.locationLabel || '' %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-medium">Holiday Season Label</label>
|
|
<input type="text" class="form-control" id="searchBarHolidaySeasonLabel"
|
|
value="<%= data.searchBar?.holidaySeasonLabel || '' %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-medium">Search Button Text</label>
|
|
<input type="text" class="form-control" id="searchBarSearchButtonText"
|
|
value="<%= data.searchBar?.searchButtonText || '' %>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sub-tabs for Locations and Holidays -->
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<ul class="nav nav-pills nav-fill" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab"
|
|
data-bs-target="#searchLocations" type="button">
|
|
<i class="fas fa-map-marker-alt me-2"></i>Locations
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab"
|
|
data-bs-target="#searchHolidays" type="button">
|
|
<i class="fas fa-calendar me-2"></i>Holidays
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
<!-- Locations Sub-tab -->
|
|
<div class="tab-pane fade show active" id="searchLocations" role="tabpanel">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Manage Locations</h6>
|
|
<button type="button" class="btn btn-primary btn-sm"
|
|
onclick="addLocation()">
|
|
<i class="fas fa-plus"></i> Add Location
|
|
</button>
|
|
</div>
|
|
<div id="locationsContainer">
|
|
<% if (data.locations && data.locations.length > 0) { %>
|
|
<% data.locations.forEach((location, index) => { %>
|
|
<div class="card mb-3 location-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control"
|
|
name="locationValue_<%= index %>"
|
|
value="<%= location.value %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control"
|
|
name="locationLabel_<%= index %>"
|
|
value="<%= location.label %>">
|
|
</div>
|
|
</div>
|
|
<button type="button"
|
|
class="btn btn-outline-danger btn-sm mt-3"
|
|
onclick="removeLocation(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Location
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Holidays Sub-tab -->
|
|
<div class="tab-pane fade" id="searchHolidays" role="tabpanel">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Manage Holiday Seasons</h6>
|
|
<button type="button" class="btn btn-primary btn-sm"
|
|
onclick="addHoliday()">
|
|
<i class="fas fa-plus"></i> Add Holiday
|
|
</button>
|
|
</div>
|
|
<div id="holidaysContainer">
|
|
<% if (data.holidays && data.holidays.length > 0) { %>
|
|
<% data.holidays.forEach((holiday, index) => { %>
|
|
<div class="card mb-3 holiday-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control"
|
|
name="holidayValue_<%= index %>"
|
|
value="<%= holiday.value %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control"
|
|
name="holidayLabel_<%= index %>"
|
|
value="<%= holiday.label %>">
|
|
</div>
|
|
</div>
|
|
<button type="button"
|
|
class="btn btn-outline-danger btn-sm mt-3"
|
|
onclick="removeHoliday(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Panel Tab -->
|
|
<div class="tab-pane fade" id="filterPanel" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-medium">Title</label>
|
|
<input type="text" class="form-control" id="filterPanelTitle"
|
|
value="<%= data.filterPanel?.title || '' %>">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
<h6 class="fw-medium mb-3">Price Settings</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Price Title</label>
|
|
<input type="text" class="form-control" id="filterPanelPriceTitle"
|
|
value="<%= data.filterPanel?.priceTitle || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Price Label</label>
|
|
<input type="text" class="form-control" id="filterPanelPriceLabel"
|
|
value="<%= data.filterPanel?.priceLabel || '' %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Price Placeholder</label>
|
|
<input type="text" class="form-control" id="filterPanelPricePlaceholder"
|
|
value="<%= data.filterPanel?.pricePlaceholder || '' %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Price Min</label>
|
|
<input type="number" class="form-control" id="filterPanelPriceMin"
|
|
value="<%= data.filterPanel?.priceMin || 0 %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Price Max</label>
|
|
<input type="number" class="form-control" id="filterPanelPriceMax"
|
|
value="<%= data.filterPanel?.priceMax || 0 %>">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
<h6 class="fw-medium mb-3">Age Settings</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Age Title</label>
|
|
<input type="text" class="form-control" id="filterPanelAgeTitle"
|
|
value="<%= data.filterPanel?.ageTitle || '' %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Age Min</label>
|
|
<input type="number" class="form-control" id="filterPanelAgeMin"
|
|
value="<%= data.filterPanel?.ageMin || 0 %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Age Max</label>
|
|
<input type="number" class="form-control" id="filterPanelAgeMax"
|
|
value="<%= data.filterPanel?.ageMax || 0 %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Age Select Placeholder</label>
|
|
<input type="text" class="form-control"
|
|
id="filterPanelAgeSelectPlaceholder"
|
|
value="<%= data.filterPanel?.ageSelectPlaceholder || '' %>">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
<h6 class="fw-medium mb-3">Other Settings</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Activities Title</label>
|
|
<input type="text" class="form-control" id="filterPanelActivitiesTitle"
|
|
value="<%= data.filterPanel?.activitiesTitle || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Rating Title</label>
|
|
<input type="text" class="form-control" id="filterPanelRatingTitle"
|
|
value="<%= data.filterPanel?.ratingTitle || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Reset Button Text</label>
|
|
<input type="text" class="form-control" id="filterPanelResetButtonText"
|
|
value="<%= data.filterPanel?.resetButtonText || '' %>">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
|
<h6 class="fw-medium mb-3">Rating Options</h6>
|
|
<button type="button" class="btn btn-primary btn-sm"
|
|
onclick="addRatingOption()">
|
|
<i class="fas fa-plus"></i> Add Rating Option
|
|
</button>
|
|
</div>
|
|
|
|
<div id="ratingOptionsContainer">
|
|
<% if (data.filterPanel?.ratingOptions && data.filterPanel.ratingOptions.length > 0) { %>
|
|
<% data.filterPanel.ratingOptions.forEach((option, index) => { %>
|
|
<div class="card mb-3 rating-option-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control"
|
|
name="ratingOptionValue_<%= index %>"
|
|
value="<%= option.value %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control"
|
|
name="ratingOptionLabel_<%= index %>"
|
|
value="<%= option.label %>">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
|
onclick="removeRatingOption(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Option
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Programs Tab -->
|
|
<div class="tab-pane fade" id="programs" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Programs</h6>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="addProgram()">
|
|
<i class="fas fa-plus"></i> Add Program
|
|
</button>
|
|
</div>
|
|
<div id="programsContainer">
|
|
<% if (data.programs && data.programs.length > 0) { %>
|
|
<% data.programs.forEach((program, index) => { %>
|
|
<div class="card mb-3 program-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control"
|
|
name="programValue_<%= index %>"
|
|
value="<%= program.value %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control"
|
|
name="programLabel_<%= index %>"
|
|
value="<%= program.label %>">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
|
onclick="removeProgram(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Program
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Camps Tab -->
|
|
<div class="tab-pane fade" id="camps" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-medium mb-0">Camps</h6>
|
|
<button type="button" class="btn btn-primary btn-sm" onclick="addCamp()">
|
|
<i class="fas fa-plus"></i> Add Camp
|
|
</button>
|
|
</div>
|
|
<div id="campsContainer">
|
|
<% if (data.camps && data.camps.length > 0) { %>
|
|
<% data.camps.forEach((camp, index) => { %>
|
|
<div class="card mb-3 camp-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control"
|
|
name="campName_<%= index %>" value="<%= camp.name %>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Price</label>
|
|
<input type="number" class="form-control"
|
|
name="campPrice_<%= index %>" value="<%= camp.price %>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Price Text</label>
|
|
<input type="text" class="form-control"
|
|
name="campPriceText_<%= index %>"
|
|
value="<%= camp.priceText %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Season (comma separated)</label>
|
|
<input type="text" class="form-control"
|
|
name="campSeason_<%= index %>"
|
|
value="<%= (camp.season || []).join(', ') %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Age (comma separated)</label>
|
|
<input type="text" class="form-control"
|
|
name="campAge_<%= index %>"
|
|
value="<%= (camp.age || []).join(', ') %>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Locations (comma
|
|
separated)</label>
|
|
<input type="text" class="form-control"
|
|
name="campLocations_<%= index %>"
|
|
value="<%= (camp.locations || []).join(', ') %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Program</label>
|
|
<input type="text" class="form-control"
|
|
name="campProgram_<%= index %>"
|
|
value="<%= camp.program %>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Rating</label>
|
|
<input type="number" step="0.1" class="form-control"
|
|
name="campRating_<%= index %>"
|
|
value="<%= camp.rating %>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Link</label>
|
|
<input type="text" class="form-control"
|
|
name="campLink_<%= index %>" value="<%= camp.link %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image</label>
|
|
<div class="row g-3">
|
|
<div class="col-md-9">
|
|
<div class="input-group">
|
|
<input type="text"
|
|
class="form-control camp-image-input"
|
|
name="campImage_<%= index %>"
|
|
id="campImage_<%= index %>"
|
|
value="<%= camp.image %>">
|
|
<button type="button"
|
|
class="btn btn-outline-primary btn-upload-image"
|
|
data-target-input="campImage_<%= index %>"
|
|
data-image-type="booking">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
|
|
<img src="<%= camp.image %>"
|
|
class="img-thumbnail camp-image-preview"
|
|
alt="Camp Image Preview"
|
|
style="max-width: 100%; max-height: 100%; object-fit: cover;">
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
|
onclick="removeCamp(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Camp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% }); %>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Discounts Tab -->
|
|
<div class="tab-pane fade" id="discounts" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0"><i class="fas fa-percent me-2"></i>Discount Management</h6>
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="addDiscount()">
|
|
<i class="fas fa-plus me-2"></i>Add Discount
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="discountsContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vouchers Tab -->
|
|
<div class="tab-pane fade" id="vouchers" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0"><i class="fas fa-ticket-alt me-2"></i>Voucher Management</h6>
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="addVoucher()">
|
|
<i class="fas fa-plus me-2"></i>Add Voucher
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="vouchersContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="form" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-light">
|
|
<ul class="nav nav-pills nav-fill" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" data-bs-toggle="tab"
|
|
data-bs-target="#formStep1" type="button">
|
|
<i class="fas fa-child me-2"></i>Step 1: Participant
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" data-bs-toggle="tab"
|
|
data-bs-target="#formStep2" type="button">
|
|
<i class="fas fa-user-shield me-2"></i>Step 2: Guardian
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
|
|
<div class="tab-pane fade show active" id="formStep1" role="tabpanel">
|
|
<div class="alert bg-primary text-white">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
Edit fields for the <strong>Participant Information</strong> step.
|
|
<small class="d-block mt-1 text-white">Field "Name" keys should not
|
|
be changed as they link to backend logic.</small>
|
|
</div>
|
|
<div id="step1Container">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="formStep2" role="tabpanel">
|
|
<div class="alert bg-primary text-white">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
Edit fields for the <strong>Guardian Information</strong> step.
|
|
<small class="d-block mt-1 text-white">Use the "Required" checkbox on each field to mark mandatory fields.</small>
|
|
</div>
|
|
<div id="step2Container">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Fixed Bottom Buttons -->
|
|
<div class="fixed-bottom-buttons">
|
|
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
|
<i class="fas fa-undo me-2"></i>Reset
|
|
</button>
|
|
<button type="submit" class="btn btn-primary" id="submitBtn"
|
|
<%= data && data._id ? '' : 'disabled' %>>
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
<% if (!data || !data._id) { %>
|
|
<% } %>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Custom styling for search bar sub-tabs */
|
|
<style>
|
|
/* Định nghĩa biến màu */
|
|
:root {
|
|
--primary-color: #bc9f69;
|
|
--primary-light: #bba57c;
|
|
--primary-dark: #be9d5f;
|
|
--secondary-color: #f5f5e8;
|
|
--text-light: black; /* Sửa #black thành black */
|
|
}
|
|
|
|
/* Áp dụng cho SearchBar */
|
|
#searchBar .nav-pills .nav-link {
|
|
color: #6c757d; /* Giữ màu xám cho trạng thái chưa kích hoạt */
|
|
background-color: transparent;
|
|
border: 1px solid #dee2e6;
|
|
margin: 0 2px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
#searchBar .nav-pills .nav-link:hover {
|
|
color: var(--primary-dark);
|
|
background-color: var(--secondary-color);
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
#searchBar .nav-pills .nav-link.active {
|
|
color: #fff; /* Hoặc dùng var(--text-light) nếu bạn muốn chữ màu đen */
|
|
/* Tạo gradient từ màu đậm sang nhạt */
|
|
background: linear-gradient(90deg, var(--primary-dark), var(--primary-color), var(--primary-light));
|
|
border: none;
|
|
}
|
|
|
|
/* Class bg-primary chung */
|
|
.bg-primary {
|
|
background: linear-gradient(90deg, var(--primary-dark), var(--primary-color), var(--primary-light)) !important;
|
|
border-color: var(--primary-color) !important;
|
|
}
|
|
|
|
/* Áp dụng cho Form (giống SearchBar) */
|
|
#form .nav-pills .nav-link {
|
|
color: #6c757d;
|
|
background-color: transparent;
|
|
border: 1px solid #dee2e6;
|
|
margin: 0 2px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
#form .nav-pills .nav-link:hover {
|
|
color: var(--primary-dark);
|
|
background-color: var(--secondary-color);
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
#form .nav-pills .nav-link.active {
|
|
color: #fff;
|
|
background: linear-gradient(90deg, var(--primary-dark), var(--primary-color), var(--primary-light));
|
|
border: none;
|
|
}
|
|
</style>
|
|
|
|
<!-- Use global toast manager from public/js/toast.js (loaded in main layout) -->
|
|
<!-- Main Form Script -->
|
|
<script>
|
|
let originalFormData = null;
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
|
|
function updateAllJsonInputs(data) {
|
|
data = data || {};
|
|
// Hero
|
|
try {
|
|
const hero = data.hero || {};
|
|
const heroTitle = document.getElementById('heroTitle');
|
|
const heroBg = document.getElementById('heroBackgroundImage');
|
|
if (heroTitle) heroTitle.value = hero.title || '';
|
|
if (heroBg) heroBg.value = hero.backgroundImage || '';
|
|
} catch (e) {
|
|
console.warn('updateAllJsonInputs hero error', e);
|
|
}
|
|
|
|
// Search bar
|
|
try {
|
|
document.getElementById('searchBarLocationLabel').value = (data.searchBar && data.searchBar
|
|
.locationLabel) || '';
|
|
document.getElementById('searchBarHolidaySeasonLabel').value = (data.searchBar && data.searchBar
|
|
.holidaySeasonLabel) || '';
|
|
document.getElementById('searchBarSearchButtonText').value = (data.searchBar && data.searchBar
|
|
.searchButtonText) || '';
|
|
} catch (e) {
|
|
/* ignore */ }
|
|
|
|
// Locations
|
|
try {
|
|
const locs = data.locations || [];
|
|
const container = document.getElementById('locationsContainer');
|
|
container.innerHTML = '';
|
|
locs.forEach((loc, i) => {
|
|
const html = `
|
|
<div class="card mb-3 location-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="locationValue_${i}" value="${(loc.value||'')}">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="locationLabel_${i}" value="${(loc.label||'')}">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeLocation(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Location
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('locations populate error', e);
|
|
}
|
|
|
|
// Holidays
|
|
try {
|
|
const list = data.holidays || [];
|
|
const container = document.getElementById('holidaysContainer');
|
|
container.innerHTML = '';
|
|
list.forEach((h, i) => {
|
|
const html = `
|
|
<div class="card mb-3 holiday-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="holidayValue_${i}" value="${(h.value||'')}">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="holidayLabel_${i}" value="${(h.label||'')}">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeHoliday(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('holidays populate error', e);
|
|
}
|
|
|
|
// Rating Options
|
|
try {
|
|
const opts = (data.filterPanel && data.filterPanel.ratingOptions) || [];
|
|
const container = document.getElementById('ratingOptionsContainer');
|
|
container.innerHTML = '';
|
|
opts.forEach((opt, i) => {
|
|
const html = `
|
|
<div class="card mb-3 rating-option-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="ratingOptionValue_${i}" value="${(opt.value||'')}">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="ratingOptionLabel_${i}" value="${(opt.label||'')}">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeRatingOption(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Option
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('rating options populate error', e);
|
|
}
|
|
|
|
// Programs
|
|
try {
|
|
const list = data.programs || [];
|
|
const container = document.getElementById('programsContainer');
|
|
container.innerHTML = '';
|
|
list.forEach((p, i) => {
|
|
const html = `
|
|
<div class="card mb-3 program-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="programValue_${i}" value="${(p.value||'')}">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="programLabel_${i}" value="${(p.label||'')}">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeProgram(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Program
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('programs populate error', e);
|
|
}
|
|
|
|
// Camps
|
|
try {
|
|
const list = data.camps || [];
|
|
const container = document.getElementById('campsContainer');
|
|
container.innerHTML = '';
|
|
list.forEach((camp, i) => {
|
|
const imageHtml = camp.image ?
|
|
`<img src="${camp.image}" class="img-thumbnail camp-image-preview" alt="Camp Image Preview" style="max-width: 100%; max-height: 100%; object-fit: contain;">` :
|
|
`<img src="" class="img-thumbnail camp-image-preview" alt="Camp Image Preview" style="display:none; max-width: 100%; max-height: 100%; object-fit: contain;">`;
|
|
const html = `
|
|
<div class="card mb-3 camp-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control" name="campName_${i}" value="${(camp.name||'')}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Price</label>
|
|
<input type="number" class="form-control" name="campPrice_${i}" value="${(camp.price||0)}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Price Text</label>
|
|
<input type="text" class="form-control" name="campPriceText_${i}" value="${(camp.priceText||'')}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Season (comma separated)</label>
|
|
<input type="text" class="form-control" name="campSeason_${i}" value="${((camp.season||[]).join(', '))}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Age (comma separated)</label>
|
|
<input type="text" class="form-control" name="campAge_${i}" value="${((camp.age||[]).join(', '))}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Locations (comma separated)</label>
|
|
<input type="text" class="form-control" name="campLocations_${i}" value="${((camp.locations||[]).join(', '))}">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Program</label>
|
|
<input type="text" class="form-control" name="campProgram_${i}" value="${(camp.program||'')}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Rating</label>
|
|
<input type="number" step="0.1" class="form-control" name="campRating_${i}" value="${(camp.rating||0)}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Link</label>
|
|
<input type="text" class="form-control" name="campLink_${i}" value="${(camp.link||'')}">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image</label>
|
|
<div class="row g-3">
|
|
<div class="col-md-9">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control camp-image-input" name="campImage_${i}" id="campImage_${i}" value="${(camp.image||'')}">
|
|
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="campImage_${i}" data-image-type="booking">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="image-preview-container" style="height: 100%; width: 100%;">
|
|
${imageHtml}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeCamp(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Camp
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('camps populate error', e);
|
|
}
|
|
|
|
// --- NEW: BOOKING FORM STEPS ---
|
|
try {
|
|
// Step 1: Participant
|
|
renderStepFields(0, 'step1Container', data);
|
|
|
|
// Step 2: Guardian
|
|
renderStepFields(1, 'step2Container', data);
|
|
|
|
} catch (e) {
|
|
console.warn('Form steps populate error', e);
|
|
}
|
|
|
|
|
|
// Discounts - check both top-level and configuration.discounts
|
|
try {
|
|
const discounts = data.discounts || (data.configuration && data.configuration.discounts) || [];
|
|
console.log('Loading discounts:', discounts);
|
|
const container = document.getElementById('discountsContainer');
|
|
container.innerHTML = '';
|
|
|
|
discounts.forEach((discount, i) => {
|
|
const id = escapeHtml(discount.id || '');
|
|
const name = escapeHtml(discount.name || '');
|
|
const type = discount.type || 'percentage';
|
|
const value = discount.value || 0;
|
|
const description = escapeHtml(discount.description || '');
|
|
|
|
const html = `
|
|
<div class="card mb-3 discount-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label">ID</label>
|
|
<input type="text" class="form-control" name="discountId_${i}" value="${id}">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control" name="discountName_${i}" value="${name}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select" name="discountType_${i}">
|
|
<option value="percentage" ${type==='percentage'?'selected':''}>Percentage</option>
|
|
<option value="fixed" ${type==='fixed'?'selected':''}>Fixed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Value</label>
|
|
<input type="number" step="0.01" class="form-control" name="discountValue_${i}" value="${value}">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" name="discountDescription_${i}" rows="2">${description}</textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeDiscount(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Discount
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('discounts populate error', e);
|
|
}
|
|
|
|
// ====== LÀM TƯƠNG TỰ CHO VOUCHERS (khoảng dòng 4794) ======
|
|
try {
|
|
const vouchers = data.vouchers || (data.configuration && data.configuration.vouchers) || [];
|
|
console.log('Loading vouchers:', vouchers);
|
|
const container = document.getElementById('vouchersContainer');
|
|
container.innerHTML = '';
|
|
|
|
vouchers.forEach((voucher, i) => {
|
|
const validCodes = escapeHtml(voucher.validCodes || '');
|
|
const type = voucher.type || 'percentage';
|
|
const value = voucher.value || 0;
|
|
|
|
const html = `
|
|
<div class="card mb-3 voucher-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Valid Code</label>
|
|
<input type="text" class="form-control" name="voucherCode_${i}" value="${validCodes}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select" name="voucherType_${i}">
|
|
<option value="percentage" ${type==='percentage'?'selected':''}>Percentage</option>
|
|
<option value="fixed" ${type==='fixed'?'selected':''}>Fixed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Value</label>
|
|
<input type="number" step="0.01" class="form-control" name="voucherValue_${i}" value="${value}">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeVoucher(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Voucher
|
|
</button>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
} catch (e) {
|
|
console.warn('vouchers populate error', e);
|
|
}
|
|
// Update hidden JSON inputs to match restored data
|
|
try {
|
|
document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
|
|
document.getElementById('searchBarJson').value = JSON.stringify(data.searchBar || {});
|
|
document.getElementById('filterPanelJson').value = JSON.stringify(data.filterPanel || {});
|
|
document.getElementById('programsJson').value = JSON.stringify(data.programs || []);
|
|
document.getElementById('holidaysJson').value = JSON.stringify(data.holidays || []);
|
|
document.getElementById('locationsJson').value = JSON.stringify(data.locations || []);
|
|
document.getElementById('campsJson').value = JSON.stringify(data.camps || []);
|
|
document.getElementById('discountsJson').value = JSON.stringify(data.discounts || (data.configuration && data.configuration.discounts) || []);
|
|
document.getElementById('vouchersJson').value = JSON.stringify(data.vouchers || (data.configuration && data.configuration.vouchers) || []);
|
|
document.getElementById('formStepsJson').value = JSON.stringify(data.formSteps || []);
|
|
document.getElementById('validationJson').value = JSON.stringify(data.validation || {});
|
|
} catch (e) {
|
|
/* ignore */ }
|
|
}
|
|
// Complete booking form script with all fields
|
|
function updateJsonData() {
|
|
try {
|
|
// Hero
|
|
document.getElementById('heroJson').value = JSON.stringify({
|
|
title: document.getElementById('heroTitle').value,
|
|
backgroundImage: document.getElementById('heroBackgroundImage').value
|
|
});
|
|
|
|
// Search Bar
|
|
document.getElementById('searchBarJson').value = JSON.stringify({
|
|
locationLabel: document.getElementById('searchBarLocationLabel').value,
|
|
holidaySeasonLabel: document.getElementById('searchBarHolidaySeasonLabel').value,
|
|
searchButtonText: document.getElementById('searchBarSearchButtonText').value
|
|
});
|
|
|
|
// Filter Panel - Rating Options
|
|
const ratingOptions = [];
|
|
document.querySelectorAll('.rating-option-item').forEach((item) => {
|
|
const valueInput = item.querySelector(`[name^="ratingOptionValue_"]`);
|
|
const labelInput = item.querySelector(`[name^="ratingOptionLabel_"]`);
|
|
|
|
if (valueInput && labelInput) {
|
|
ratingOptions.push({
|
|
value: valueInput.value,
|
|
label: labelInput.value
|
|
});
|
|
}
|
|
});
|
|
|
|
document.getElementById('filterPanelJson').value = JSON.stringify({
|
|
title: document.getElementById('filterPanelTitle').value,
|
|
priceTitle: document.getElementById('filterPanelPriceTitle').value,
|
|
priceLabel: document.getElementById('filterPanelPriceLabel').value,
|
|
pricePlaceholder: document.getElementById('filterPanelPricePlaceholder').value,
|
|
priceMin: parseFloat(document.getElementById('filterPanelPriceMin').value) || 0,
|
|
priceMax: parseFloat(document.getElementById('filterPanelPriceMax').value) || 0,
|
|
activitiesTitle: document.getElementById('filterPanelActivitiesTitle').value,
|
|
ageTitle: document.getElementById('filterPanelAgeTitle').value,
|
|
ageSelectPlaceholder: document.getElementById('filterPanelAgeSelectPlaceholder').value,
|
|
ageMin: parseInt(document.getElementById('filterPanelAgeMin').value) || 0,
|
|
ageMax: parseInt(document.getElementById('filterPanelAgeMax').value) || 0,
|
|
ratingTitle: document.getElementById('filterPanelRatingTitle').value,
|
|
ratingOptions: ratingOptions,
|
|
resetButtonText: document.getElementById('filterPanelResetButtonText').value
|
|
});
|
|
|
|
// Programs
|
|
const programs = [];
|
|
document.querySelectorAll('.program-item').forEach((item) => {
|
|
const valueInput = item.querySelector(`[name^="programValue_"]`);
|
|
const labelInput = item.querySelector(`[name^="programLabel_"]`);
|
|
|
|
if (valueInput && labelInput) {
|
|
programs.push({
|
|
value: valueInput.value,
|
|
label: labelInput.value
|
|
});
|
|
}
|
|
});
|
|
document.getElementById('programsJson').value = JSON.stringify(programs);
|
|
|
|
// Holidays
|
|
const holidays = [];
|
|
document.querySelectorAll('.holiday-item').forEach((item) => {
|
|
const valueInput = item.querySelector(`[name^="holidayValue_"]`);
|
|
const labelInput = item.querySelector(`[name^="holidayLabel_"]`);
|
|
|
|
if (valueInput && labelInput) {
|
|
holidays.push({
|
|
value: valueInput.value,
|
|
label: labelInput.value
|
|
});
|
|
}
|
|
});
|
|
document.getElementById('holidaysJson').value = JSON.stringify(holidays);
|
|
|
|
// Locations
|
|
const locations = [];
|
|
document.querySelectorAll('.location-item').forEach((item) => {
|
|
const valueInput = item.querySelector(`[name^="locationValue_"]`);
|
|
const labelInput = item.querySelector(`[name^="locationLabel_"]`);
|
|
|
|
if (valueInput && labelInput) {
|
|
locations.push({
|
|
value: valueInput.value,
|
|
label: labelInput.value
|
|
});
|
|
}
|
|
});
|
|
document.getElementById('locationsJson').value = JSON.stringify(locations);
|
|
|
|
// Camps
|
|
const camps = [];
|
|
document.querySelectorAll('.camp-item').forEach((item) => {
|
|
const nameInput = item.querySelector(`[name^="campName_"]`);
|
|
const priceInput = item.querySelector(`[name^="campPrice_"]`);
|
|
const priceTextInput = item.querySelector(`[name^="campPriceText_"]`);
|
|
const seasonInput = item.querySelector(`[name^="campSeason_"]`);
|
|
const ageInput = item.querySelector(`[name^="campAge_"]`);
|
|
const locationsInput = item.querySelector(`[name^="campLocations_"]`);
|
|
const imageInput = item.querySelector(`[name^="campImage_"]`);
|
|
const linkInput = item.querySelector(`[name^="campLink_"]`);
|
|
const programInput = item.querySelector(`[name^="campProgram_"]`);
|
|
const ratingInput = item.querySelector(`[name^="campRating_"]`);
|
|
|
|
if (nameInput) {
|
|
const seasonValue = seasonInput ? seasonInput.value : '';
|
|
const ageValue = ageInput ? ageInput.value : '';
|
|
const locationsValue = locationsInput ? locationsInput.value : '';
|
|
|
|
camps.push({
|
|
name: nameInput.value,
|
|
price: priceInput ? (parseFloat(priceInput.value) || 0) : 0,
|
|
priceText: priceTextInput ? priceTextInput.value : '',
|
|
season: seasonValue ? seasonValue.split(',').map(s => s.trim()).filter(s => s) :
|
|
[],
|
|
age: ageValue ? ageValue.split(',').map(a => parseInt(a.trim())).filter(a => !
|
|
isNaN(a)) : [],
|
|
locations: locationsValue ? locationsValue.split(',').map(l => l.trim()).filter(
|
|
l => l) : [],
|
|
image: imageInput ? imageInput.value : '',
|
|
link: linkInput ? linkInput.value : '',
|
|
program: programInput ? programInput.value : '',
|
|
rating: ratingInput ? (parseFloat(ratingInput.value) || 0) : 0
|
|
});
|
|
}
|
|
});
|
|
document.getElementById('campsJson').value = JSON.stringify(camps);
|
|
|
|
// Discounts
|
|
const discounts = [];
|
|
document.querySelectorAll('.discount-item').forEach((item) => {
|
|
const idInput = item.querySelector(`[name^="discountId_"]`);
|
|
const nameInput = item.querySelector(`[name^="discountName_"]`);
|
|
const typeInput = item.querySelector(`[name^="discountType_"]`);
|
|
const valueInput = item.querySelector(`[name^="discountValue_"]`);
|
|
const descInput = item.querySelector(`[name^="discountDescription_"]`);
|
|
|
|
if (nameInput) {
|
|
discounts.push({
|
|
id: idInput ? idInput.value : '',
|
|
name: nameInput.value,
|
|
type: typeInput ? typeInput.value : 'percentage',
|
|
value: valueInput ? (parseFloat(valueInput.value) || 0) : 0,
|
|
description: descInput ? descInput.value : ''
|
|
});
|
|
}
|
|
});
|
|
document.getElementById('discountsJson').value = JSON.stringify(discounts);
|
|
|
|
// Vouchers
|
|
const vouchers = [];
|
|
document.querySelectorAll('.voucher-item').forEach((item) => {
|
|
const codeInput = item.querySelector(`[name^="voucherCode_"]`);
|
|
const typeInput = item.querySelector(`[name^="voucherType_"]`);
|
|
const valueInput = item.querySelector(`[name^="voucherValue_"]`);
|
|
|
|
if (codeInput) {
|
|
vouchers.push({
|
|
validCodes: codeInput.value,
|
|
type: typeInput ? typeInput.value : 'percentage',
|
|
value: valueInput ? (parseFloat(valueInput.value) || 0) : 0
|
|
});
|
|
}
|
|
});
|
|
document.getElementById('vouchersJson').value = JSON.stringify(vouchers);
|
|
|
|
// Form Steps - collect from editor
|
|
const formSteps = [];
|
|
const step1Fields = collectStepFields(0);
|
|
const step2Fields = collectStepFields(1);
|
|
|
|
if (step1Fields) formSteps.push(step1Fields);
|
|
if (step2Fields) formSteps.push(step2Fields);
|
|
|
|
document.getElementById('formStepsJson').value = JSON.stringify(formSteps);
|
|
|
|
// Validation - auto-generate from required checkboxes
|
|
const validation = {
|
|
step1Required: [],
|
|
step2Required: []
|
|
};
|
|
|
|
// Collect required field names from Step 1
|
|
if (step1Fields && step1Fields.sections) {
|
|
step1Fields.sections.forEach(section => {
|
|
section.fields.forEach(field => {
|
|
if (field.required) {
|
|
validation.step1Required.push(field.name);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Collect required field names from Step 2
|
|
if (step2Fields && step2Fields.sections) {
|
|
step2Fields.sections.forEach(section => {
|
|
section.fields.forEach(field => {
|
|
if (field.required) {
|
|
validation.step2Required.push(field.name);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
document.getElementById('validationJson').value = JSON.stringify(validation);
|
|
|
|
console.log('All data updated successfully!');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Error updating JSON data:', error);
|
|
if (window.toastManager) {
|
|
window.toastManager.error('Error updating data: ' + error.message);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Add Rating Option
|
|
function addRatingOption() {
|
|
const container = document.getElementById('ratingOptionsContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 rating-option-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="ratingOptionValue_${index}" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="ratingOptionLabel_${index}" value="">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeRatingOption(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Option
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Rating option added successfully');
|
|
}
|
|
|
|
function removeRatingOption(btn) {
|
|
btn.closest('.rating-option-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Rating option removed successfully');
|
|
}
|
|
|
|
// Add Program
|
|
function addProgram() {
|
|
const container = document.getElementById('programsContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 program-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="programValue_${index}" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="programLabel_${index}" value="">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeProgram(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Program
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Program added successfully');
|
|
}
|
|
|
|
function removeProgram(btn) {
|
|
btn.closest('.program-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Program removed successfully');
|
|
}
|
|
|
|
// Add Holiday
|
|
function addHoliday() {
|
|
const container = document.getElementById('holidaysContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 holiday-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="holidayValue_${index}" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="holidayLabel_${index}" value="">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeHoliday(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Holiday
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Holiday added successfully');
|
|
}
|
|
|
|
function removeHoliday(btn) {
|
|
btn.closest('.holiday-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Holiday removed successfully');
|
|
}
|
|
|
|
// Add Location
|
|
function addLocation() {
|
|
const container = document.getElementById('locationsContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 location-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Value</label>
|
|
<input type="text" class="form-control" name="locationValue_${index}" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Label</label>
|
|
<input type="text" class="form-control" name="locationLabel_${index}" value="">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeLocation(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Location
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Location added successfully');
|
|
}
|
|
|
|
function removeLocation(btn) {
|
|
btn.closest('.location-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Location removed successfully');
|
|
}
|
|
|
|
// Add Camp
|
|
function addCamp() {
|
|
const container = document.getElementById('campsContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 camp-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control" name="campName_${index}" value="">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Price</label>
|
|
<input type="number" class="form-control" name="campPrice_${index}" value="0">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Price Text</label>
|
|
<input type="text" class="form-control" name="campPriceText_${index}" value="">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Season (comma separated)</label>
|
|
<input type="text" class="form-control" name="campSeason_${index}" value="">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Age (comma separated)</label>
|
|
<input type="text" class="form-control" name="campAge_${index}" value="">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Locations (comma separated)</label>
|
|
<input type="text" class="form-control" name="campLocations_${index}" value="">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Program</label>
|
|
<input type="text" class="form-control" name="campProgram_${index}" value="">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Rating</label>
|
|
<input type="number" step="0.1" class="form-control" name="campRating_${index}" value="0">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Link</label>
|
|
<input type="text" class="form-control" name="campLink_${index}" value="">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Image</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" name="campImage_${index}" id="campImage_${index}" value="">
|
|
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
data-target-input="campImage_${index}" data-image-type="booking">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeCamp(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Camp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Camp added successfully');
|
|
}
|
|
|
|
function removeCamp(btn) {
|
|
btn.closest('.camp-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Camp removed successfully');
|
|
}
|
|
|
|
// Add Discount
|
|
function addDiscount() {
|
|
const container = document.getElementById('discountsContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 discount-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label">ID</label>
|
|
<input type="text" class="form-control" name="discountId_${index}" value="">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label">Name</label>
|
|
<input type="text" class="form-control" name="discountName_${index}" value="">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select" name="discountType_${index}">
|
|
<option value="percentage">Percentage</option>
|
|
<option value="fixed">Fixed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">Value</label>
|
|
<input type="number" step="0.01" class="form-control" name="discountValue_${index}" value="0">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" name="discountDescription_${index}" rows="2"></textarea>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeDiscount(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Discount
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Discount added successfully');
|
|
}
|
|
|
|
function removeDiscount(btn) {
|
|
btn.closest('.discount-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Discount removed successfully');
|
|
}
|
|
|
|
// Add Voucher
|
|
function addVoucher() {
|
|
const container = document.getElementById('vouchersContainer');
|
|
const index = Date.now();
|
|
const html = `
|
|
<div class="card mb-3 voucher-item">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Valid Code</label>
|
|
<input type="text" class="form-control" name="voucherCode_${index}" value="">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Type</label>
|
|
<select class="form-select" name="voucherType_${index}">
|
|
<option value="percentage">Percentage</option>
|
|
<option value="fixed">Fixed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Value</label>
|
|
<input type="number" step="0.01" class="form-control" name="voucherValue_${index}" value="0">
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeVoucher(this)">
|
|
<i class="fas fa-trash me-2"></i>Remove Voucher
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('afterbegin', html);
|
|
if (window.toastManager) window.toastManager.success('Voucher added successfully');
|
|
}
|
|
|
|
function removeVoucher(btn) {
|
|
btn.closest('.voucher-item').remove();
|
|
if (window.toastManager) window.toastManager.success('Voucher removed successfully');
|
|
}
|
|
|
|
// Toggle field options visibility based on field type
|
|
function toggleFieldOptions(selectElement) {
|
|
const card = selectElement.closest('.field-item');
|
|
const optionsContainer = card.querySelector('.field-options-container');
|
|
const fieldType = selectElement.value;
|
|
|
|
if (fieldType === 'select' || fieldType === 'checkbox-group' || fieldType === 'radio-group') {
|
|
optionsContainer.classList.remove('d-none');
|
|
} else {
|
|
optionsContainer.classList.add('d-none');
|
|
}
|
|
}
|
|
|
|
// Add new field to section
|
|
function addFieldToSection(stepIndex, sectionIdx) {
|
|
const containerId = stepIndex === 0 ? 'step1Container' : 'step2Container';
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
// Find the section header and insert after all its fields
|
|
const sectionHeaders = container.querySelectorAll('.section-header');
|
|
const targetSection = sectionHeaders[sectionIdx];
|
|
|
|
// Find where to insert (before next section or at end)
|
|
let insertPosition = null;
|
|
if (sectionIdx < sectionHeaders.length - 1) {
|
|
insertPosition = sectionHeaders[sectionIdx + 1];
|
|
}
|
|
|
|
const fieldId = `s${stepIndex}_sec${sectionIdx}_f${Date.now()}`;
|
|
const newFieldHtml = `
|
|
<div class="card mb-3 field-item bg-light border-0 shadow-sm" data-step="${stepIndex}" data-section="${sectionIdx}">
|
|
<div class="card-body p-3">
|
|
<div class="row g-3">
|
|
<input type="hidden" class="field-name" value="newField_${Date.now()}">
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label small text-muted">
|
|
<i class="fas fa-key me-1"></i>Field Name (System Key)
|
|
</label>
|
|
<input type="text" class="form-control form-control-sm bg-white" value="newField_${Date.now()}" readonly>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-bold">
|
|
<i class="fas fa-tag me-1"></i>Display Label
|
|
</label>
|
|
<input type="text" class="form-control form-control-sm field-label" value="New Field" placeholder="Enter field label">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small">
|
|
<i class="fas fa-list-ul me-1"></i>Type
|
|
</label>
|
|
<select class="form-select form-select-sm field-type" onchange="toggleFieldOptions(this)">
|
|
<option value="text" selected>Text</option>
|
|
<option value="email">Email</option>
|
|
<option value="tel">Phone</option>
|
|
<option value="date">Date</option>
|
|
<option value="select">Select</option>
|
|
<option value="checkbox-group">Checkbox Group</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 field-options-container d-none">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label small mb-0">
|
|
<i class="fas fa-list-alt me-1"></i>Options
|
|
</label>
|
|
<button type="button" class="btn btn-xs btn-outline-primary" onclick="addOptionToField(this)">
|
|
<i class="fas fa-plus me-1"></i>Add Option
|
|
</button>
|
|
</div>
|
|
<div class="options-list">
|
|
<!-- Options will be added here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 d-flex justify-content-between align-items-center mt-2 pt-2 border-top">
|
|
<div class="form-check">
|
|
<input class="form-check-input field-required" type="checkbox" id="req_${fieldId}">
|
|
<label class="form-check-label small" for="req_${fieldId}">
|
|
<i class="fas fa-asterisk text-danger me-1" style="font-size: 8px;"></i>Required
|
|
</label>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeField(this)">
|
|
<i class="fas fa-trash me-1"></i>Remove Field
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
if (insertPosition) {
|
|
insertPosition.insertAdjacentHTML('beforebegin', newFieldHtml);
|
|
} else {
|
|
container.insertAdjacentHTML('beforeend', newFieldHtml);
|
|
}
|
|
|
|
if (window.toastManager) window.toastManager.success('Field added successfully');
|
|
}
|
|
|
|
// Remove field
|
|
function removeField(btn) {
|
|
if (confirm('Are you sure you want to remove this field?')) {
|
|
const fieldCard = btn.closest('.field-item');
|
|
fieldCard.remove();
|
|
if (window.toastManager) window.toastManager.success('Field removed successfully');
|
|
}
|
|
}
|
|
|
|
// Add option to field
|
|
function addOptionToField(btn) {
|
|
const fieldCard = btn.closest('.field-item');
|
|
const optionsList = fieldCard.querySelector('.options-list');
|
|
|
|
const optionHtml = `
|
|
<div class="card mb-2 option-item border">
|
|
<div class="card-body p-2">
|
|
<div class="row g-2">
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control form-control-sm option-value" placeholder="Value (e.g., opt1)">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<input type="text" class="form-control form-control-sm option-label" placeholder="Label (e.g., Option 1)">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<input type="number" class="form-control form-control-sm option-price" placeholder="Price" value="0">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="removeOption(this)" title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
optionsList.insertAdjacentHTML('beforeend', optionHtml);
|
|
if (window.toastManager) window.toastManager.success('Option added');
|
|
}
|
|
|
|
// Remove option
|
|
function removeOption(btn) {
|
|
const optionCard = btn.closest('.option-item');
|
|
optionCard.remove();
|
|
if (window.toastManager) window.toastManager.success('Option removed');
|
|
}
|
|
|
|
// Reset Form
|
|
function resetForm() {
|
|
if (confirm('Are you sure you want to reset all changes?')) {
|
|
if (originalFormData) {
|
|
updateAllJsonInputs(originalFormData);
|
|
location.reload();
|
|
return;
|
|
}
|
|
document.getElementById('bookingForm').reset();
|
|
if (window.toastManager) window.toastManager.success('Form reset to initial values successfully');
|
|
}
|
|
}
|
|
|
|
// Form initialization
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
console.log('DOM loaded, initializing booking form...');
|
|
try {
|
|
// load original data from server-rendered `data` object
|
|
originalFormData = <%- JSON.stringify(data || {}) %>;
|
|
updateAllJsonInputs(originalFormData);
|
|
} catch (e) {
|
|
console.warn('Could not initialize originalFormData', e);
|
|
}
|
|
|
|
const form = document.getElementById('bookingForm');
|
|
|
|
if (form) {
|
|
form.addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
|
|
|
try {
|
|
if (updateJsonData()) {
|
|
if (window.toastManager) {
|
|
window.toastManager.info('Saving changes...');
|
|
}
|
|
// Submit form after brief delay
|
|
setTimeout(() => {
|
|
this.submit();
|
|
}, 300);
|
|
} else {
|
|
throw new Error('Failed to update JSON data');
|
|
}
|
|
} catch (error) {
|
|
console.error('Form submission error:', error);
|
|
if (window.toastManager) {
|
|
window.toastManager.error('Error: ' + error.message);
|
|
}
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalText;
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('Booking form initialized successfully');
|
|
});
|
|
|
|
// Helper to collect step fields from editor
|
|
function collectStepFields(stepIndex) {
|
|
const containerId = stepIndex === 0 ? 'step1Container' : 'step2Container';
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return null;
|
|
|
|
const fieldItems = container.querySelectorAll('.field-item');
|
|
if (fieldItems.length === 0) return null;
|
|
|
|
// Get original step structure
|
|
const originalStep = originalFormData?.formSteps?.[stepIndex];
|
|
if (!originalStep) return null;
|
|
|
|
const step = {
|
|
step: stepIndex + 1,
|
|
title: originalStep.title,
|
|
sections: []
|
|
};
|
|
|
|
// Group fields by section
|
|
const sectionMap = {};
|
|
fieldItems.forEach(item => {
|
|
const sectionIdx = parseInt(item.getAttribute('data-section') || '0');
|
|
if (!sectionMap[sectionIdx]) {
|
|
sectionMap[sectionIdx] = {
|
|
id: originalStep.sections[sectionIdx]?.id || `section_${sectionIdx}`,
|
|
fields: []
|
|
};
|
|
}
|
|
|
|
const field = {
|
|
name: item.querySelector('.field-name')?.value || '',
|
|
label: item.querySelector('.field-label')?.value || '',
|
|
type: item.querySelector('.field-type')?.value || 'text',
|
|
required: item.querySelector('.field-required')?.checked || false
|
|
};
|
|
|
|
// Add options if needed - collect from option fields
|
|
const optionItems = item.querySelectorAll('.option-item');
|
|
if (optionItems.length > 0) {
|
|
field.options = [];
|
|
optionItems.forEach(optItem => {
|
|
const value = optItem.querySelector('.option-value')?.value || '';
|
|
const label = optItem.querySelector('.option-label')?.value || '';
|
|
const price = parseFloat(optItem.querySelector('.option-price')?.value) || 0;
|
|
|
|
if (value || label) {
|
|
field.options.push({ value, label, price });
|
|
}
|
|
});
|
|
}
|
|
|
|
sectionMap[sectionIdx].fields.push(field);
|
|
});
|
|
|
|
// Convert map to array
|
|
step.sections = Object.values(sectionMap);
|
|
return step;
|
|
}
|
|
|
|
// Helper to render field HTML
|
|
function renderStepFields(stepIndex, containerId, data) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
|
|
// Safety check
|
|
if (!data.formSteps || !data.formSteps[stepIndex] || !data.formSteps[stepIndex].sections) {
|
|
container.innerHTML = `
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>No form fields configured yet.</strong>
|
|
<p class="mb-0 mt-2">Please add formSteps data to <code>data/booking.json</code> or save this form to initialize the structure.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
data.formSteps[stepIndex].sections.forEach((section, secIdx) => {
|
|
// Section Header with controls
|
|
const sectionHtml = `
|
|
<div class="d-flex justify-content-between align-items-center mt-4 mb-3 section-header" data-section-idx="${secIdx}">
|
|
<h6 class="border-bottom pb-2 mb-0 text-uppercase flex-grow-1">${section.id.replace('_', ' ')}</h6>
|
|
<button type="button" class="btn btn btn-primary btn-sm mb-4" onclick="addFieldToSection(${stepIndex}, ${secIdx})">
|
|
<i class="fas fa-plus me-1"></i>Add Field
|
|
</button>
|
|
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', sectionHtml);
|
|
|
|
section.fields.forEach((field, fIdx) => {
|
|
const fieldId = `s${stepIndex}_sec${secIdx}_f${fIdx}`;
|
|
|
|
// Format options as JSON string for editing if it's a select/checkbox
|
|
const optionsJson = field.options ? JSON.stringify(field.options) : '';
|
|
const showOptions = field.type === 'select' || field.type === 'checkbox-group' || field
|
|
.type === 'radio-group';
|
|
|
|
const html = `
|
|
<div class="card mb-3 field-item bg-light border-0 shadow-sm" data-step="${stepIndex}" data-section="${secIdx}">
|
|
<div class="card-body p-3">
|
|
<div class="row g-3">
|
|
<input type="hidden" class="field-name" value="${field.name}">
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label small text-muted">
|
|
<i class="fas fa-key me-1"></i>Field Name (System Key)
|
|
</label>
|
|
<input type="text" class="form-control form-control-sm bg-white" value="${field.name}" readonly>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-bold">
|
|
<i class="fas fa-tag me-1"></i>Display Label
|
|
</label>
|
|
<input type="text" class="form-control form-control-sm field-label" value="${field.label || ''}">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small">
|
|
<i class="fas fa-list-ul me-1"></i>Type
|
|
</label>
|
|
<select class="form-select form-select-sm field-type" onchange="toggleFieldOptions(this)">
|
|
<option value="text" ${field.type === 'text' ? 'selected' : ''}>Text</option>
|
|
<option value="email" ${field.type === 'email' ? 'selected' : ''}>Email</option>
|
|
<option value="tel" ${field.type === 'tel' ? 'selected' : ''}>Phone</option>
|
|
<option value="date" ${field.type === 'date' ? 'selected' : ''}>Date</option>
|
|
<option value="select" ${field.type === 'select' ? 'selected' : ''}>Select</option>
|
|
<option value="checkbox-group" ${field.type === 'checkbox-group' ? 'selected' : ''}>Checkbox Group</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 field-options-container ${showOptions ? '' : 'd-none'}">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label small mb-0">
|
|
<i class="fas fa-list-alt me-1"></i>Options
|
|
</label>
|
|
<button type="button" class="btn btn-xs btn-outline-primary" onclick="addOptionToField(this)">
|
|
<i class="fas fa-plus me-1"></i>Add Option
|
|
</button>
|
|
</div>
|
|
<div class="options-list">
|
|
${(field.options || []).map((opt, optIdx) => `
|
|
<div class="card mb-2 option-item border">
|
|
<div class="card-body p-2">
|
|
<div class="row g-2">
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control form-control-sm option-value" placeholder="Value" value="${opt.value || ''}">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<input type="text" class="form-control form-control-sm option-label" placeholder="Label" value="${opt.label || ''}">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<input type="number" class="form-control form-control-sm option-price" placeholder="Price" value="${opt.price || 0}">
|
|
</div>
|
|
<div class="col-md-1">
|
|
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="removeOption(this)" title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 d-flex justify-content-between align-items-center mt-2 pt-2 border-top">
|
|
<div class="form-check">
|
|
<input class="form-check-input color-primary field-required" type="checkbox" id="req_${fieldId}" ${field.required ? 'checked' : ''}>
|
|
<label class="form-check-label small " for="req_${fieldId}">
|
|
<sup><i class="fas fa-asterisk text-danger me-1" style="font-size: 8px;"></i></sup>Required
|
|
</label>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeField(this)">
|
|
<i class="fas fa-trash me-1"></i>Remove Field
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
// Image upload helper: uses event delegation on `.btn-upload-image`
|
|
async function openImageUploader(targetInputId, imageType) {
|
|
try {
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = 'image/*';
|
|
|
|
fileInput.addEventListener('change', async function () {
|
|
const file = this.files && this.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
const uploadUrl =
|
|
`/admin/upload/image?imageType=${encodeURIComponent(imageType || 'booking')}`;
|
|
|
|
try {
|
|
|
|
const resp = await fetch(uploadUrl, {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
const json = await resp.json();
|
|
if (!resp.ok || !json || !json.success) {
|
|
const msg = (json && json.message) || ('Upload failed with status ' + resp
|
|
.status);
|
|
if (window.toastManager) window.toastManager.error(msg);
|
|
return;
|
|
}
|
|
|
|
// server returns path in json.path (e.g. /uploads/...) or something similar
|
|
const path = json.path || json.data || '';
|
|
const input = document.getElementById(targetInputId);
|
|
if (input) {
|
|
input.value = path;
|
|
|
|
// update nearby preview if any
|
|
let previewImg = null;
|
|
// try to find an img in the same card / container
|
|
const card = input.closest('.card') || input.closest('div');
|
|
if (card) previewImg = card.querySelector('img');
|
|
|
|
if (previewImg) {
|
|
// make path absolute if necessary
|
|
const src = (path && path.startsWith('/')) ? (window.location.origin +
|
|
path) : path;
|
|
previewImg.src = src;
|
|
// ensure preview is visible if it was hidden
|
|
try {
|
|
previewImg.style.display = '';
|
|
} catch (e) {
|
|
/* ignore */ }
|
|
}
|
|
|
|
|
|
if (window.toastManager) window.toastManager.success('Upload completed successfully');
|
|
} else {
|
|
if (window.toastManager) window.toastManager.success('Upload completed successfully');
|
|
}
|
|
} catch (err) {
|
|
console.error('Upload error', err);
|
|
if (window.toastManager) window.toastManager.error('Upload error: ' + (err.message ||
|
|
err));
|
|
}
|
|
});
|
|
|
|
// trigger file chooser
|
|
fileInput.click();
|
|
} catch (err) {
|
|
console.error('openImageUploader error', err);
|
|
if (window.toastManager) window.toastManager.error('Error: ' + (err.message || err));
|
|
}
|
|
}
|
|
|
|
// Event delegation: handle clicks on any `.btn-upload-image`, including dynamically added ones
|
|
document.addEventListener('click', function (e) {
|
|
const btn = e.target.closest && e.target.closest('.btn-upload-image');
|
|
if (!btn) return;
|
|
const targetInput = btn.getAttribute('data-target-input');
|
|
const imageType = btn.getAttribute('data-image-type') || 'layout';
|
|
if (!targetInput) {
|
|
if (window.toastManager) window.toastManager.error('Upload target input not specified');
|
|
return;
|
|
}
|
|
openImageUploader(targetInput, imageType);
|
|
});
|
|
</script>
|