feat: Оптимизация навигации AdminJS в логические группы

- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
2025-11-30 21:57:58 +09:00
parent 1e7d7c06eb
commit 13c752b93a
47 changed files with 14148 additions and 61 deletions

262
views/routes/booking.ejs Normal file
View 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>