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

557 lines
19 KiB
HTML
Raw 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>Календарь гидов</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: #f8f9fa;
}
.calendar-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.calendar-header {
background: #2d3748;
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.calendar-title {
font-size: 24px;
font-weight: 600;
margin: 0;
}
.month-navigation {
display: flex;
align-items: center;
gap: 15px;
}
.nav-button {
background: rgba(255,255,255,0.1);
border: none;
color: white;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.nav-button:hover {
background: rgba(255,255,255,0.2);
}
.current-month {
font-size: 18px;
font-weight: 600;
min-width: 200px;
text-align: center;
}
.calendar-content {
padding: 20px;
}
.guides-filter {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.filter-label {
font-weight: 600;
color: #495057;
}
.guide-checkbox {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
}
.guide-checkbox:hover {
border-color: #ff6b6b;
background: #fff5f5;
}
.guide-checkbox input[type="checkbox"] {
margin: 0;
}
.guide-checkbox.checked {
border-color: #ff6b6b;
background: #fff5f5;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background: #dee2e6;
border-radius: 8px;
overflow: hidden;
}
.day-header {
background: #6c757d;
color: white;
padding: 10px;
text-align: center;
font-weight: 600;
font-size: 14px;
}
.day-cell {
background: white;
min-height: 120px;
padding: 8px;
position: relative;
display: flex;
flex-direction: column;
}
.day-cell.other-month {
background: #f8f9fa;
color: #6c757d;
}
.day-cell.today {
background: #fff3cd;
}
.day-number {
font-weight: 600;
margin-bottom: 5px;
color: #495057;
}
.day-cell.other-month .day-number {
color: #adb5bd;
}
.guide-schedule {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.guide-item {
background: #e9ecef;
color: #495057;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.guide-item.working {
background: #d1f2eb;
color: #00875a;
}
.guide-item.holiday {
background: #ffcdd2;
color: #d32f2f;
}
.guide-item.partial {
background: #fff3cd;
color: #856404;
}
.guide-item:hover {
transform: scale(1.05);
}
.legend {
display: flex;
gap: 20px;
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
justify-content: center;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
}
.working { background: #d1f2eb; }
.holiday { background: #ffcdd2; }
.partial { background: #fff3cd; }
.unavailable { background: #e9ecef; }
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.error {
text-align: center;
padding: 40px;
color: #dc3545;
background: #f8d7da;
border-radius: 8px;
margin: 20px 0;
}
@media (max-width: 768px) {
.calendar-header {
flex-direction: column;
gap: 15px;
}
.guides-filter {
justify-content: center;
}
.day-cell {
min-height: 80px;
padding: 4px;
}
.guide-item {
font-size: 10px;
padding: 1px 4px;
}
}
</style>
</head>
<body>
<div class="calendar-container">
<div class="calendar-header">
<h1 class="calendar-title">📅 Календарь работы гидов</h1>
<div class="month-navigation">
<button class="nav-button" onclick="changeMonth(-1)"></button>
<div class="current-month" id="currentMonth"></div>
<button class="nav-button" onclick="changeMonth(1)"></button>
</div>
</div>
<div class="calendar-content">
<div class="guides-filter">
<span class="filter-label">Показать гидов:</span>
<div id="guidesFilter"></div>
</div>
<div id="calendarGrid"></div>
<div class="legend">
<div class="legend-item">
<div class="legend-color working"></div>
<span>Рабочий день</span>
</div>
<div class="legend-item">
<div class="legend-color partial"></div>
<span>Частично доступен</span>
</div>
<div class="legend-item">
<div class="legend-color holiday"></div>
<span>Выходной</span>
</div>
<div class="legend-item">
<div class="legend-color unavailable"></div>
<span>Не работает</span>
</div>
</div>
</div>
</div>
<script>
class GuideCalendar {
constructor() {
this.currentDate = new Date();
this.guides = [];
this.schedules = [];
this.holidays = [];
this.selectedGuides = new Set();
this.bookings = [];
this.init();
}
async init() {
await this.loadData();
this.renderGuidesFilter();
this.renderCalendar();
this.updateMonthDisplay();
}
async loadData() {
try {
// Загружаем гидов
const guidesResponse = await fetch('/api/guides');
const guidesData = await guidesResponse.json();
this.guides = Array.isArray(guidesData) ? guidesData : (guidesData.data || guidesData.guides || []);
// Загружаем расписания
const schedulesResponse = await fetch('/api/guide-schedules');
const schedulesData = await schedulesResponse.json();
this.schedules = Array.isArray(schedulesData) ? schedulesData : (schedulesData.data || schedulesData.schedules || []);
// Загружаем выходные дни
const holidaysResponse = await fetch('/api/holidays');
const holidaysData = await holidaysResponse.json();
this.holidays = Array.isArray(holidaysData) ? holidaysData : (holidaysData.data || holidaysData.holidays || []);
// Загружаем существующие бронирования
const bookingsResponse = await fetch('/api/bookings');
const bookingsData = await bookingsResponse.json();
this.bookings = Array.isArray(bookingsData) ? bookingsData : (bookingsData.data || bookingsData.bookings || []);
// По умолчанию показываем всех гидов
if (this.guides && this.guides.length > 0) {
this.guides.forEach(guide => this.selectedGuides.add(guide.id));
}
} catch (error) {
console.error('Ошибка загрузки данных:', error);
document.getElementById('calendarGrid').innerHTML =
'<div class="error">Ошибка загрузки данных календаря</div>';
}
}
renderGuidesFilter() {
const filterContainer = document.getElementById('guidesFilter');
filterContainer.innerHTML = '';
if (!this.guides || !Array.isArray(this.guides)) {
filterContainer.innerHTML = '<div class="error">Нет доступных гидов</div>';
return;
}
this.guides.forEach(guide => {
const checkbox = document.createElement('label');
checkbox.className = 'guide-checkbox';
if (this.selectedGuides.has(guide.id)) {
checkbox.classList.add('checked');
}
checkbox.innerHTML = `
<input type="checkbox"
${this.selectedGuides.has(guide.id) ? 'checked' : ''}
onchange="calendar.toggleGuide(${guide.id})">
<span>${guide.name}</span>
`;
filterContainer.appendChild(checkbox);
});
}
toggleGuide(guideId) {
if (this.selectedGuides.has(guideId)) {
this.selectedGuides.delete(guideId);
} else {
this.selectedGuides.add(guideId);
}
this.renderGuidesFilter();
this.renderCalendar();
}
renderCalendar() {
const grid = document.getElementById('calendarGrid');
grid.innerHTML = '';
grid.className = 'calendar-grid';
// Заголовки дней недели
const dayHeaders = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
dayHeaders.forEach(day => {
const header = document.createElement('div');
header.className = 'day-header';
header.textContent = day;
grid.appendChild(header);
});
// Дни месяца
const firstDay = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth(), 1);
const lastDay = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 0);
// Начинаем с понедельника
const startDate = new Date(firstDay);
const dayOfWeek = firstDay.getDay();
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
startDate.setDate(startDate.getDate() - daysToSubtract);
// Генерируем 42 дня (6 недель)
for (let i = 0; i < 42; i++) {
const currentDay = new Date(startDate);
currentDay.setDate(startDate.getDate() + i);
const dayCell = this.createDayCell(currentDay);
grid.appendChild(dayCell);
}
}
createDayCell(date) {
const cell = document.createElement('div');
cell.className = 'day-cell';
const isCurrentMonth = date.getMonth() === this.currentDate.getMonth();
const isToday = this.isToday(date);
if (!isCurrentMonth) {
cell.classList.add('other-month');
}
if (isToday) {
cell.classList.add('today');
}
const dayNumber = document.createElement('div');
dayNumber.className = 'day-number';
dayNumber.textContent = date.getDate();
cell.appendChild(dayNumber);
const scheduleContainer = document.createElement('div');
scheduleContainer.className = 'guide-schedule';
// Добавляем информацию о гидах для этого дня
this.guides.forEach(guide => {
if (this.selectedGuides.has(guide.id)) {
const guideStatus = this.getGuideStatusForDate(guide, date);
const guideItem = document.createElement('div');
guideItem.className = `guide-item ${guideStatus.type}`;
guideItem.textContent = `${guide.name.split(' ')[0]} ${guideStatus.time}`;
guideItem.title = `${guide.name} - ${guideStatus.description}`;
scheduleContainer.appendChild(guideItem);
}
});
cell.appendChild(scheduleContainer);
return cell;
}
getGuideStatusForDate(guide, date) {
const dayOfWeek = date.getDay();
const dayName = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'][dayOfWeek];
// Проверяем выходные дни
const holiday = this.holidays.find(h =>
h.guide_id === guide.id &&
new Date(h.date).toDateString() === date.toDateString()
);
if (holiday) {
return {
type: 'holiday',
time: '',
description: holiday.title
};
}
// Проверяем расписание
const schedule = this.schedules.find(s => s.guide_id === guide.id);
if (!schedule || !schedule[dayName]) {
return {
type: 'unavailable',
time: '',
description: 'Не работает'
};
}
// Проверяем существующие бронирования
const dateStr = date.toISOString().split('T')[0];
const dayBookings = this.bookings.filter(b =>
b.guide_id === guide.id &&
b.booking_date === dateStr
);
const startTime = schedule.start_time || '09:00';
const endTime = schedule.end_time || '18:00';
if (dayBookings.length > 0) {
// Если есть бронирования, показываем частично доступен
return {
type: 'partial',
time: `${startTime}-${endTime}`,
description: `Рабочий день (${dayBookings.length} бронирований)`
};
}
return {
type: 'working',
time: `${startTime}-${endTime}`,
description: `Рабочий день ${startTime}-${endTime}`
};
}
isToday(date) {
const today = new Date();
return date.toDateString() === today.toDateString();
}
updateMonthDisplay() {
const monthNames = [
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
];
const monthDisplay = document.getElementById('currentMonth');
monthDisplay.textContent = `${monthNames[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`;
}
changeMonth(delta) {
this.currentDate.setMonth(this.currentDate.getMonth() + delta);
this.renderCalendar();
this.updateMonthDisplay();
}
}
// Инициализация календаря
document.addEventListener('DOMContentLoaded', () => {
calendar = new GuideCalendar();
// Добавляем обработчики событий для кнопок навигации
const prevBtn = document.getElementById('prevMonth');
const nextBtn = document.getElementById('nextMonth');
if (prevBtn) {
prevBtn.addEventListener('click', () => calendar.changeMonth(-1));
}
if (nextBtn) {
nextBtn.addEventListener('click', () => calendar.changeMonth(1));
}
});
</script>
</body>
</html>