- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
625 lines
20 KiB
HTML
625 lines
20 KiB
HTML
<!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> |