feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
262
views/routes/booking.ejs
Normal file
262
views/routes/booking.ejs
Normal file
@@ -0,0 +1,262 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section compact bg-primary text-white py-5">
|
||||
<div class="container text-center">
|
||||
<h1 class="display-4 fw-bold mb-3">Бронирование тура</h1>
|
||||
<p class="lead">Выберите дату и гида для вашего незабываемого путешествия</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container py-5">
|
||||
<div class="row">
|
||||
<!-- Информация о маршруте -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0"><%= route.title %></h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<%
|
||||
let placeholderImage = '/images/placeholder.jpg';
|
||||
if (route.type === 'city') {
|
||||
placeholderImage = '/images/city-tour-placeholder.webp';
|
||||
} else if (route.type === 'mountain') {
|
||||
placeholderImage = '/images/mountain-placeholder.jpg';
|
||||
} else if (route.type === 'fishing') {
|
||||
placeholderImage = '/images/fish-placeholder.jpg';
|
||||
}
|
||||
%>
|
||||
<img src="<%= route.image_url || placeholderImage %>"
|
||||
class="img-fluid rounded"
|
||||
alt="<%= route.title %>">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<p class="text-muted mb-3"><%= route.description %></p>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-clock text-primary me-2"></i>
|
||||
<span><%= route.duration %> дней</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-users text-primary me-2"></i>
|
||||
<span>До <%= route.max_group_size %> человек</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-star text-primary me-2"></i>
|
||||
<span><%= route.difficulty_level === 'easy' ? 'Легкий' : route.difficulty_level === 'moderate' ? 'Средний' : 'Сложный' %></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-tag text-primary me-2"></i>
|
||||
<span class="h5 text-primary mb-0">₩<%= formatCurrency(route.price) %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор даты и проверка доступности -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-calendar-check me-2"></i>Проверка доступности</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="route-availability-checker"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Выбор гида -->
|
||||
<div class="card mb-4" id="guide-selection-card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-user-tie me-2"></i>Выбор гида</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="route-guide-selector"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма бронирования -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card sticky-top" style="top: 20px;">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-credit-card me-2"></i>Детали бронирования</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/bookings" method="POST" id="mainBookingForm">
|
||||
<input type="hidden" name="route_id" value="<%= route.id %>">
|
||||
<input type="hidden" name="guide_id" id="selectedGuideId">
|
||||
<input type="hidden" name="preferred_date" id="selectedDate">
|
||||
|
||||
<!-- Выбранные детали -->
|
||||
<div id="booking-summary" class="mb-4 p-3 bg-light rounded" style="display: none;">
|
||||
<h6 class="fw-bold mb-2">Выбрано:</h6>
|
||||
<div id="summary-content"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="people_count" class="form-label">Количество человек</label>
|
||||
<input type="number" class="form-control" name="people_count" id="people_count"
|
||||
min="1" max="<%= route.max_group_size %>" value="1" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="customer_name" class="form-label">Ваше имя *</label>
|
||||
<input type="text" class="form-control" name="customer_name" id="customer_name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="customer_email" class="form-label">Email *</label>
|
||||
<input type="email" class="form-control" name="customer_email" id="customer_email" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="customer_phone" class="form-label">Телефон *</label>
|
||||
<input type="tel" class="form-control" name="customer_phone" id="customer_phone" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="special_requirements" class="form-label">Особые пожелания</label>
|
||||
<textarea class="form-control" name="special_requirements" id="special_requirements" rows="3"
|
||||
placeholder="Любые специальные запросы или требования..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Итоговая стоимость -->
|
||||
<div class="mb-4 p-3 bg-primary bg-opacity-10 rounded">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">Итого:</span>
|
||||
<span class="h5 text-primary mb-0" id="total-price">₩<%= formatCurrency(route.price) %></span>
|
||||
</div>
|
||||
<small class="text-muted">За <span id="people-display">1</span> человека</small>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submitBookingBtn" disabled>
|
||||
<i class="fas fa-credit-card me-2"></i>Забронировать тур
|
||||
</button>
|
||||
<a href="/routes/<%= route.id %>" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Назад к описанию
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const routePrice = <%= route.price %>;
|
||||
const maxGroupSize = <%= route.max_group_size %>;
|
||||
|
||||
// Компонент проверки доступности
|
||||
const availabilityChecker = new AvailabilityChecker({
|
||||
container: document.getElementById('route-availability-checker'),
|
||||
mode: 'detailed',
|
||||
showSuggestions: true,
|
||||
onAvailabilityCheck: function(result) {
|
||||
if (result.availableGuides && result.availableGuides.length > 0) {
|
||||
showGuideSelection(result.availableGuides, result.date);
|
||||
} else {
|
||||
hideGuideSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция показа секции выбора гида
|
||||
function showGuideSelection(availableGuides, selectedDate) {
|
||||
const guideCard = document.getElementById('guide-selection-card');
|
||||
const guideSelectorContainer = document.getElementById('route-guide-selector');
|
||||
|
||||
guideCard.style.display = 'block';
|
||||
|
||||
const guideSelector = new GuideSelector({
|
||||
container: guideSelectorContainer,
|
||||
mode: 'booking',
|
||||
showAvailability: false,
|
||||
availableGuides: availableGuides,
|
||||
selectedDate: selectedDate,
|
||||
onGuideSelect: function(guide) {
|
||||
updateBookingSummary(guide, selectedDate);
|
||||
enableBookingForm(guide.id, selectedDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция скрытия секции выбора гида
|
||||
function hideGuideSelection() {
|
||||
const guideCard = document.getElementById('guide-selection-card');
|
||||
guideCard.style.display = 'none';
|
||||
disableBookingForm();
|
||||
}
|
||||
|
||||
// Обновление сводки бронирования
|
||||
function updateBookingSummary(guide, date) {
|
||||
const summaryContainer = document.getElementById('booking-summary');
|
||||
const summaryContent = document.getElementById('summary-content');
|
||||
|
||||
summaryContent.innerHTML = `
|
||||
<div class="mb-2">
|
||||
<strong>Дата:</strong> ${formatDate(date)}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Гид:</strong> ${guide.name}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Специализация:</strong> ${guide.specialization || 'Универсальный'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
summaryContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
// Активация формы бронирования
|
||||
function enableBookingForm(guideId, date) {
|
||||
document.getElementById('selectedGuideId').value = guideId;
|
||||
document.getElementById('selectedDate').value = date;
|
||||
document.getElementById('submitBookingBtn').disabled = false;
|
||||
}
|
||||
|
||||
// Деактивация формы бронирования
|
||||
function disableBookingForm() {
|
||||
document.getElementById('selectedGuideId').value = '';
|
||||
document.getElementById('selectedDate').value = '';
|
||||
document.getElementById('submitBookingBtn').disabled = true;
|
||||
document.getElementById('booking-summary').style.display = 'none';
|
||||
}
|
||||
|
||||
// Обновление общей стоимости
|
||||
const peopleCountInput = document.getElementById('people_count');
|
||||
const totalPriceElement = document.getElementById('total-price');
|
||||
const peopleDisplayElement = document.getElementById('people-display');
|
||||
|
||||
peopleCountInput.addEventListener('input', function() {
|
||||
const peopleCount = parseInt(this.value) || 1;
|
||||
const totalPrice = routePrice * peopleCount;
|
||||
|
||||
totalPriceElement.textContent = `₩${new Intl.NumberFormat('ru-RU').format(totalPrice)}`;
|
||||
peopleDisplayElement.textContent = peopleCount;
|
||||
});
|
||||
|
||||
// Функция форматирования даты
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -41,7 +41,7 @@
|
||||
<strong class="d-block">Максимум участников</strong>
|
||||
<span><%= route.max_group_size %> человек</span>
|
||||
</div>
|
||||
<a href="/contact" class="btn btn-primary w-100">
|
||||
<a href="/routes/<%= route.id %>/booking" class="btn btn-primary w-100">
|
||||
<i class="fas fa-calendar-plus me-1"></i>Забронировать
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user