- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
462 lines
16 KiB
HTML
462 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Календарь гидов</title>
|
||
<style>
|
||
.calendar-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
.calendar-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.calendar-navigation {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.nav-button {
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 15px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.nav-button:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
.current-date {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #343a40;
|
||
min-width: 200px;
|
||
text-align: center;
|
||
}
|
||
|
||
.guides-filter {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-label {
|
||
font-weight: 600;
|
||
color: #495057;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.guide-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 8px 12px;
|
||
border: 2px solid #dee2e6;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
background: white;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.guide-checkbox:hover {
|
||
border-color: #007bff;
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.guide-checkbox.checked {
|
||
background: #007bff;
|
||
color: white;
|
||
border-color: #007bff;
|
||
}
|
||
|
||
.guide-checkbox input[type="checkbox"] {
|
||
display: none;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 1px;
|
||
background: #dee2e6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.calendar-day {
|
||
background: white;
|
||
min-height: 120px;
|
||
padding: 8px;
|
||
position: relative;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.calendar-day.other-month {
|
||
background: #f8f9fa;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.calendar-day.today {
|
||
background: #fff3cd;
|
||
border-color: #ffeaa7;
|
||
}
|
||
|
||
.day-number {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.day-header {
|
||
background: #343a40;
|
||
color: white;
|
||
padding: 15px 8px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.guide-status {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
}
|
||
|
||
.guide-badge {
|
||
padding: 2px 6px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.guide-badge.working {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.guide-badge.holiday {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.guide-badge.busy {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.legend {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 30px;
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.legend-color {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
margin: 20px 0;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="calendar-container">
|
||
<div class="calendar-header">
|
||
<div class="calendar-navigation">
|
||
<button class="nav-button" id="prevMonth">‹</button>
|
||
<span class="current-date" id="currentDate"></span>
|
||
<button class="nav-button" id="nextMonth">›</button>
|
||
</div>
|
||
|
||
<div class="guides-filter">
|
||
<span class="filter-label">Фильтр гидов:</span>
|
||
<div id="guidesFilter"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="calendarGrid" class="calendar-grid"></div>
|
||
|
||
<div class="legend">
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: #d4edda;"></div>
|
||
<span>Работает</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: #f8d7da;"></div>
|
||
<span>Выходной</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="legend-color" style="background: #fff3cd;"></div>
|
||
<span>Забронирован</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
class GuideCalendar {
|
||
constructor() {
|
||
this.currentDate = new Date();
|
||
this.guides = [];
|
||
this.schedules = [];
|
||
this.holidays = [];
|
||
this.bookings = [];
|
||
this.selectedGuides = new Set();
|
||
|
||
this.init();
|
||
}
|
||
|
||
async init() {
|
||
await this.loadData();
|
||
this.renderGuidesFilter();
|
||
this.renderCalendar();
|
||
this.updateMonthDisplay();
|
||
this.bindEvents();
|
||
}
|
||
|
||
bindEvents() {
|
||
const prevBtn = document.getElementById('prevMonth');
|
||
const nextBtn = document.getElementById('nextMonth');
|
||
|
||
if (prevBtn) {
|
||
prevBtn.addEventListener('click', () => this.changeMonth(-1));
|
||
}
|
||
if (nextBtn) {
|
||
nextBtn.addEventListener('click', () => this.changeMonth(1));
|
||
}
|
||
}
|
||
|
||
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' : ''}
|
||
data-guide-id="${guide.id}">
|
||
<span>${guide.name}</span>
|
||
`;
|
||
|
||
checkbox.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
this.toggleGuide(guide.id);
|
||
});
|
||
|
||
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 = '';
|
||
|
||
// Заголовки дней недели
|
||
const dayHeaders = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||
dayHeaders.forEach(day => {
|
||
const headerDiv = document.createElement('div');
|
||
headerDiv.className = 'day-header';
|
||
headerDiv.textContent = day;
|
||
grid.appendChild(headerDiv);
|
||
});
|
||
|
||
const year = this.currentDate.getFullYear();
|
||
const month = this.currentDate.getMonth();
|
||
|
||
// Первый день месяца
|
||
const firstDay = new Date(year, month, 1);
|
||
const lastDay = new Date(year, month + 1, 0);
|
||
|
||
// Первый понедельник на календаре
|
||
const startDate = new Date(firstDay);
|
||
const dayOfWeek = firstDay.getDay();
|
||
const mondayOffset = dayOfWeek === 0 ? -6 : -(dayOfWeek - 1);
|
||
startDate.setDate(firstDay.getDate() + mondayOffset);
|
||
|
||
// Генерируем 6 недель
|
||
for (let week = 0; week < 6; week++) {
|
||
for (let day = 0; day < 7; day++) {
|
||
const currentDay = new Date(startDate);
|
||
currentDay.setDate(startDate.getDate() + week * 7 + day);
|
||
|
||
const dayDiv = document.createElement('div');
|
||
dayDiv.className = 'calendar-day';
|
||
|
||
if (currentDay.getMonth() !== month) {
|
||
dayDiv.classList.add('other-month');
|
||
}
|
||
|
||
if (this.isToday(currentDay)) {
|
||
dayDiv.classList.add('today');
|
||
}
|
||
|
||
dayDiv.innerHTML = this.renderDay(currentDay);
|
||
grid.appendChild(dayDiv);
|
||
}
|
||
}
|
||
}
|
||
|
||
renderDay(date) {
|
||
const dayNumber = date.getDate();
|
||
const dateStr = this.formatDate(date);
|
||
|
||
let guideStatusHtml = '';
|
||
|
||
// Получаем статусы выбранных гидов для этого дня
|
||
this.guides.forEach(guide => {
|
||
if (!this.selectedGuides.has(guide.id)) return;
|
||
|
||
const status = this.getGuideStatus(guide.id, dateStr);
|
||
const statusClass = status === 'holiday' ? 'holiday' :
|
||
status === 'busy' ? 'busy' : 'working';
|
||
|
||
guideStatusHtml += `<div class="guide-badge ${statusClass}">${guide.name.split(' ')[0]}</div>`;
|
||
});
|
||
|
||
return `
|
||
<div class="day-number">${dayNumber}</div>
|
||
<div class="guide-status">${guideStatusHtml}</div>
|
||
`;
|
||
}
|
||
|
||
getGuideStatus(guideId, dateStr) {
|
||
// Проверяем выходные дни
|
||
const holiday = this.holidays.find(h =>
|
||
h.guide_id === guideId && h.holiday_date === dateStr
|
||
);
|
||
if (holiday) return 'holiday';
|
||
|
||
// Проверяем бронирования
|
||
const booking = this.bookings.find(b =>
|
||
b.guide_id === guideId &&
|
||
this.formatDate(new Date(b.preferred_date)) === dateStr
|
||
);
|
||
if (booking) return 'busy';
|
||
|
||
return 'working';
|
||
}
|
||
|
||
formatDate(date) {
|
||
return date.toISOString().split('T')[0];
|
||
}
|
||
|
||
isToday(date) {
|
||
const today = new Date();
|
||
return date.toDateString() === today.toDateString();
|
||
}
|
||
|
||
updateMonthDisplay() {
|
||
const monthDisplay = document.getElementById('currentDate');
|
||
const monthNames = [
|
||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
|
||
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
|
||
];
|
||
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', () => {
|
||
new GuideCalendar();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |