Files
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

2034 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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
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 thành công');
} else {
if (window.toastManager) window.toastManager.success('Upload thành công');
}
} catch (err) {
console.error('Upload error', err);
if (window.toastManager) window.toastManager.error('Lỗi upload: ' + (err.message ||
err));
}
});
// trigger file chooser
fileInput.click();
} catch (err) {
console.error('openImageUploader error', err);
if (window.toastManager) window.toastManager.error('Lỗi: ' + (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>