feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
557
public/guide-calendar.html
Normal file
557
public/guide-calendar.html
Normal file
@@ -0,0 +1,557 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user