feat: Оптимизация навигации AdminJS в логические группы

- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
2025-11-30 21:57:58 +09:00
parent 1e7d7c06eb
commit 13c752b93a
47 changed files with 14148 additions and 61 deletions

557
public/guide-calendar.html Normal file
View 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>