Files
tourrism_site/public/tours-calendar.html
Andrey K. Choi 13c752b93a feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
2025-11-30 21:57:58 +09:00

625 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Календарь туров - Korea Tourism</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.calendar-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1rem;
}
.calendar-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.nav-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
}
.nav-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.3);
}
.month-title {
font-size: 1.8rem;
color: #333;
font-weight: bold;
}
.calendar-grid {
padding: 20px;
}
.week-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
margin-bottom: 10px;
}
.week-day {
padding: 15px;
text-align: center;
font-weight: bold;
color: #666;
background: #f8f9fa;
border-radius: 8px;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
}
.day-cell {
min-height: 120px;
padding: 12px;
border-radius: 12px;
background: #fafafa;
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
position: relative;
}
.day-cell:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
}
.day-cell.empty {
background: transparent;
cursor: default;
}
.day-cell.empty:hover {
transform: none;
box-shadow: none;
}
.day-cell.has-tours {
background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%);
border-color: #4caf50;
}
.day-cell.has-tours:hover {
border-color: #2e7d32;
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.3);
}
.day-number {
font-weight: bold;
font-size: 1.1rem;
color: #333;
margin-bottom: 8px;
}
.day-cell.has-tours .day-number {
color: #2e7d32;
}
.tours-count {
font-size: 12px;
color: #666;
margin-bottom: 6px;
}
.tour-types {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.tour-type {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.tour-city { background: #2196f3; }
.tour-mountain { background: #4caf50; }
.tour-fishing { background: #ff5722; }
.guides-info {
font-size: 11px;
color: #888;
margin-top: 4px;
}
/* Модальное окно */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 1000;
backdrop-filter: blur(5px);
}
.modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 16px;
padding: 30px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 25px 50px rgba(0,0,0,0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.modal-title {
font-size: 1.5rem;
color: #333;
font-weight: bold;
}
.close-btn {
width: 35px;
height: 35px;
border: none;
border-radius: 50%;
background: #f44336;
color: white;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.close-btn:hover {
background: #d32f2f;
transform: rotate(90deg);
}
.tour-card {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s;
border-left: 4px solid #ddd;
}
.tour-card:hover {
transform: translateX(5px);
box-shadow: 0 6px 15px rgba(0,0,0,0.1);
}
.tour-card.city {
border-left-color: #2196f3;
background: linear-gradient(135deg, #e3f2fd 0%, #f8f9fa 100%);
}
.tour-card.mountain {
border-left-color: #4caf50;
background: linear-gradient(135deg, #e8f5e8 0%, #f8f9fa 100%);
}
.tour-card.fishing {
border-left-color: #ff5722;
background: linear-gradient(135deg, #ffebee 0%, #f8f9fa 100%);
}
.tour-title {
font-size: 1.2rem;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.tour-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
.tour-detail {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #666;
}
.tour-description {
color: #777;
line-height: 1.5;
margin-top: 10px;
}
.book-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
margin-top: 10px;
transition: all 0.3s;
}
.book-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
}
.no-tours {
text-align: center;
padding: 40px;
color: #888;
}
.legend {
display: flex;
justify-content: center;
gap: 20px;
padding: 20px;
background: #f8f9fa;
margin-top: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
}
@media (max-width: 768px) {
.calendar-nav {
flex-direction: column;
gap: 15px;
}
.days-grid {
gap: 5px;
}
.day-cell {
min-height: 80px;
padding: 8px;
}
}
</style>
</head>
<body>
<div class="calendar-container">
<div class="header">
<h1>🗓️ Календарь туров</h1>
<p>Выберите дату и найдите доступные туры с лучшими гидами Кореи</p>
</div>
<div class="calendar-nav">
<button class="nav-btn" onclick="changeMonth(-1)">← Предыдущий месяц</button>
<div class="month-title" id="currentMonth">Загрузка...</div>
<button class="nav-btn" onclick="changeMonth(1)">Следующий месяц →</button>
</div>
<div class="calendar-grid">
<div class="week-header">
<div class="week-day">ПН</div>
<div class="week-day">ВТ</div>
<div class="week-day">СР</div>
<div class="week-day">ЧТ</div>
<div class="week-day">ПТ</div>
<div class="week-day">СБ</div>
<div class="week-day">ВС</div>
</div>
<div class="days-grid" id="calendarDays">
<!-- Дни будут добавлены через JavaScript -->
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color tour-city"></div>
<span>Городские туры</span>
</div>
<div class="legend-item">
<div class="legend-color tour-mountain"></div>
<span>Горные походы</span>
</div>
<div class="legend-item">
<div class="legend-color tour-fishing"></div>
<span>Морская рыбалка</span>
</div>
</div>
</div>
<!-- Модальное окно -->
<div id="tourModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="modalTitle">Туры на выбранную дату</div>
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<div id="modalContent">
<!-- Содержимое будет добавлено через JavaScript -->
</div>
</div>
</div>
<script>
let currentDate = new Date();
let calendarData = {};
const monthNames = [
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
];
async function init() {
await loadCalendarData();
renderCalendar();
}
async function loadCalendarData() {
try {
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const response = await fetch(`/api/tours-calendar?year=${year}&month=${month}`);
const data = await response.json();
if (data.success) {
calendarData = {};
data.data.forEach(item => {
calendarData[item.work_date] = item;
});
}
} catch (error) {
console.error('Ошибка загрузки календарных данных:', error);
}
}
function renderCalendar() {
document.getElementById('currentMonth').textContent =
monthNames[currentDate.getMonth()] + ' ' + currentDate.getFullYear();
const calendarDays = document.getElementById('calendarDays');
calendarDays.innerHTML = '';
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDay = new Date(year, month, 1).getDay();
const startDay = firstDay === 0 ? 6 : firstDay - 1;
// Пустые дни в начале
for (let i = 0; i < startDay; i++) {
const dayCell = document.createElement('div');
dayCell.className = 'day-cell empty';
calendarDays.appendChild(dayCell);
}
// Дни месяца
for (let day = 1; day <= daysInMonth; day++) {
const dayCell = document.createElement('div');
dayCell.className = 'day-cell';
const dateStr = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(day).padStart(2, '0');
const dayData = calendarData[dateStr];
const dayNumber = document.createElement('div');
dayNumber.className = 'day-number';
dayNumber.textContent = day;
dayCell.appendChild(dayNumber);
if (dayData) {
dayCell.classList.add('has-tours');
dayCell.onclick = () => openTourModal(dateStr);
const toursCount = document.createElement('div');
toursCount.className = 'tours-count';
toursCount.textContent = `${dayData.routes_count} туров`;
dayCell.appendChild(toursCount);
const tourTypes = document.createElement('div');
tourTypes.className = 'tour-types';
const specializations = new Set();
dayData.guides_data.forEach(guide => {
if (guide.specialization) {
specializations.add(guide.specialization);
}
});
specializations.forEach(spec => {
const tourType = document.createElement('div');
tourType.className = `tour-type tour-${spec}`;
tourTypes.appendChild(tourType);
});
dayCell.appendChild(tourTypes);
const guidesInfo = document.createElement('div');
guidesInfo.className = 'guides-info';
guidesInfo.textContent = `${dayData.guides_count} гидов`;
dayCell.appendChild(guidesInfo);
}
calendarDays.appendChild(dayCell);
}
}
async function openTourModal(date) {
try {
const response = await fetch(`/api/tours-by-date?date=${date}`);
const data = await response.json();
if (!data.success) {
alert('Ошибка загрузки туров');
return;
}
const modal = document.getElementById('tourModal');
const modalTitle = document.getElementById('modalTitle');
const modalContent = document.getElementById('modalContent');
const dateObj = new Date(date);
const formattedDate = dateObj.toLocaleDateString('ru-RU', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
modalTitle.textContent = `Туры на ${formattedDate}`;
if (data.data.length === 0) {
modalContent.innerHTML = `
<div class="no-tours">
<h3>На эту дату туры недоступны</h3>
<p>Выберите другую дату или свяжитесь с нами для индивидуального тура</p>
</div>
`;
} else {
modalContent.innerHTML = data.data.map(tour => `
<div class="tour-card ${tour.type}">
<div class="tour-title">${tour.title}</div>
<div class="tour-details">
<div class="tour-detail">
👤 <strong>${tour.guide_name}</strong>
</div>
<div class="tour-detail">
💰 ${tour.price ? tour.price + ' ₩' : 'По запросу'}
</div>
<div class="tour-detail">
${tour.duration ? tour.duration + ' часов' : 'По договоренности'}
</div>
<div class="tour-detail">
🎯 ${getTypeLabel(tour.type)}
</div>
</div>
${tour.description ? `<div class="tour-description">${tour.description.substring(0, 200)}...</div>` : ''}
${tour.guide_notes ? `<div style="font-style: italic; color: #666; margin-top: 8px;">Заметки гида: ${tour.guide_notes}</div>` : ''}
<button class="book-btn" onclick="bookTour(${tour.id}, '${date}')">
📞 Забронировать тур
</button>
</div>
`).join('');
}
modal.style.display = 'block';
} catch (error) {
console.error('Ошибка загрузки туров:', error);
alert('Ошибка загрузки туров');
}
}
function closeModal() {
document.getElementById('tourModal').style.display = 'none';
}
function getTypeLabel(type) {
const types = {
'city': 'Городские экскурсии',
'mountain': 'Горные походы',
'fishing': 'Морская рыбалка'
};
return types[type] || type;
}
function bookTour(tourId, date) {
// Здесь можно добавить логику бронирования
alert(`Функция бронирования тура #${tourId} на ${date} будет добавлена позже`);
closeModal();
}
async function changeMonth(delta) {
currentDate.setMonth(currentDate.getMonth() + delta);
await loadCalendarData();
renderCalendar();
}
// Закрытие модального окна при клике вне его
window.onclick = function(event) {
const modal = document.getElementById('tourModal');
if (event.target === modal) {
closeModal();
}
}
// Инициализация
init();
</script>
</body>
</html>