/** * GuideCalendarWidget - Переиспользуемый компонент календаря гидов * Может использоваться на фронтенде для бронирования и в админке */ class GuideCalendarWidget { constructor(options = {}) { this.container = options.container || document.body; this.mode = options.mode || 'booking'; // 'booking', 'admin', 'readonly' this.onDateSelect = options.onDateSelect || null; this.onGuideSelect = options.onGuideSelect || null; this.showGuideFilter = options.showGuideFilter !== false; this.showLegend = options.showLegend !== false; this.compact = options.compact || false; this.selectedDate = options.selectedDate || null; this.selectedGuideId = options.selectedGuideId || null; this.currentDate = new Date(); this.guides = []; this.schedules = []; this.holidays = []; this.bookings = []; this.selectedGuides = new Set(); this.init(); } async init() { this.render(); await this.loadData(); this.renderGuidesFilter(); this.renderCalendar(); this.updateMonthDisplay(); this.bindEvents(); } render() { const compactClass = this.compact ? 'calendar-compact' : ''; const modeClass = `calendar-mode-${this.mode}`; this.container.innerHTML = `
${this.showGuideFilter ? `
Гиды:
` : ''}
${this.showLegend ? `
Доступен
Выходной
Занят
${this.mode === 'booking' ? `
Выбранная дата
` : ''}
` : ''}
`; this.injectStyles(); } getId() { if (!this._id) { this._id = 'calendar-' + Math.random().toString(36).substr(2, 9); } return this._id; } injectStyles() { if (document.getElementById('guide-calendar-styles')) return; const styles = ` `; document.head.insertAdjacentHTML('beforeend', styles); } bindEvents() { this.container.addEventListener('click', (e) => { if (e.target.matches('[data-action="prev-month"]')) { this.changeMonth(-1); } else if (e.target.matches('[data-action="next-month"]')) { this.changeMonth(1); } else if (e.target.closest('.guide-checkbox')) { const checkbox = e.target.closest('.guide-checkbox'); const guideId = parseInt(checkbox.dataset.guideId); this.toggleGuide(guideId); } else if (e.target.closest('.calendar-day')) { const dayEl = e.target.closest('.calendar-day'); const dateStr = dayEl.dataset.date; if (dateStr && this.mode === 'booking') { this.selectDate(dateStr); } } }); } async loadData() { try { // Загружаем гидов const guidesResponse = await fetch('/api/guides'); const guidesData = await guidesResponse.json(); this.guides = Array.isArray(guidesData) ? guidesData : (guidesData.data || []); // Загружаем остальные данные параллельно const [schedulesRes, holidaysRes, bookingsRes] = await Promise.all([ fetch('/api/guide-schedules'), fetch('/api/holidays'), fetch('/api/bookings') ]); const schedulesData = await schedulesRes.json(); const holidaysData = await holidaysRes.json(); const bookingsData = await bookingsRes.json(); this.schedules = Array.isArray(schedulesData) ? schedulesData : (schedulesData.data || []); this.holidays = Array.isArray(holidaysData) ? holidaysData : (holidaysData.data || []); this.bookings = Array.isArray(bookingsData) ? bookingsData : (bookingsData.data || []); // Инициализируем выбранных гидов if (this.guides && this.guides.length > 0) { if (this.selectedGuideId) { this.selectedGuides.add(this.selectedGuideId); } else { this.guides.forEach(guide => this.selectedGuides.add(guide.id)); } } } catch (error) { console.error('Ошибка загрузки данных календаря:', error); this.showError('Ошибка загрузки данных календаря'); } } showError(message) { const gridEl = this.container.querySelector(`#calendarGrid-${this.getId()}`); if (gridEl) { gridEl.innerHTML = `
${message}
`; } } renderGuidesFilter() { if (!this.showGuideFilter) return; const filterContainer = this.container.querySelector(`#guidesFilter-${this.getId()}`); if (!filterContainer) return; filterContainer.innerHTML = ''; if (!this.guides || !Array.isArray(this.guides) || this.guides.length === 0) { filterContainer.innerHTML = '
Нет доступных гидов
'; return; } this.guides.forEach(guide => { const checkbox = document.createElement('label'); checkbox.className = 'guide-checkbox'; checkbox.dataset.guideId = guide.id; if (this.selectedGuides.has(guide.id)) { checkbox.classList.add('checked'); } checkbox.innerHTML = ` ${guide.name.split(' ')[0]} `; filterContainer.appendChild(checkbox); }); } toggleGuide(guideId) { if (this.selectedGuides.has(guideId)) { this.selectedGuides.delete(guideId); } else { this.selectedGuides.add(guideId); } this.renderGuidesFilter(); this.renderCalendar(); if (this.onGuideSelect) { this.onGuideSelect(Array.from(this.selectedGuides)); } } selectDate(dateStr) { this.selectedDate = dateStr; this.renderCalendar(); if (this.onDateSelect) { this.onDateSelect(dateStr); } } renderCalendar() { const grid = this.container.querySelector(`#calendarGrid-${this.getId()}`); if (!grid) return; 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 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'; dayDiv.dataset.date = this.formatDate(currentDay); if (currentDay.getMonth() !== month) { dayDiv.classList.add('other-month'); } if (this.isToday(currentDay)) { dayDiv.classList.add('today'); } if (this.selectedDate === this.formatDate(currentDay)) { dayDiv.classList.add('selected'); } 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 += `
${guide.name.split(' ')[0]}
`; }); return `
${dayNumber}
${guideStatusHtml}
`; } getStatusText(status) { const statusMap = { 'working': 'Доступен', 'holiday': 'Выходной', 'busy': 'Занят' }; return statusMap[status] || 'Неизвестно'; } 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 = this.container.querySelector(`#currentDate-${this.getId()}`); if (!monthDisplay) return; const monthNames = [ 'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь' ]; monthDisplay.textContent = `${monthNames[this.currentDate.getMonth()]} ${this.currentDate.getFullYear()}`; } changeMonth(delta) { this.currentDate.setMonth(this.currentDate.getMonth() + delta); this.renderCalendar(); this.updateMonthDisplay(); } // Публичные методы для внешнего управления setSelectedDate(dateStr) { this.selectedDate = dateStr; this.renderCalendar(); } setSelectedGuide(guideId) { this.selectedGuides.clear(); this.selectedGuides.add(guideId); this.renderGuidesFilter(); this.renderCalendar(); } getAvailableGuides(dateStr) { return this.guides.filter(guide => this.getGuideStatus(guide.id, dateStr) === 'working' ); } refresh() { this.loadData().then(() => { this.renderGuidesFilter(); this.renderCalendar(); }); } } // Экспортируем для использования в других файлах if (typeof module !== 'undefined' && module.exports) { module.exports = GuideCalendarWidget; } // Глобальная доступность в браузере if (typeof window !== 'undefined') { window.GuideCalendarWidget = GuideCalendarWidget; }