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

View File

@@ -0,0 +1,306 @@
/**
* AvailabilityChecker - Компонент для проверки доступности гидов
* Используется в формах бронирования для быстрой проверки
*/
class AvailabilityChecker {
constructor(options = {}) {
this.container = options.container || document.body;
this.mode = options.mode || 'simple'; // 'simple', 'detailed', 'inline'
this.onAvailabilityCheck = options.onAvailabilityCheck || null;
this.showSuggestions = options.showSuggestions !== false;
this.maxSuggestions = options.maxSuggestions || 3;
this.guides = [];
this.schedules = [];
this.holidays = [];
this.bookings = [];
this.init();
}
async init() {
this.render();
await this.loadData();
this.bindEvents();
}
render() {
const modeClass = `availability-checker-${this.mode}`;
this.container.innerHTML = `
<div class="availability-checker ${modeClass}">
${this.mode === 'detailed' ? `
<div class="checker-header">
<h4>Проверка доступности</h4>
<p>Укажите дату и тип тура для проверки доступности гидов</p>
</div>
` : ''}
<div class="checker-form" id="checkerForm-${this.getId()}">
<div class="form-row">
<div class="form-group">
<label for="checkDate-${this.getId()}">Дата тура:</label>
<input type="date"
id="checkDate-${this.getId()}"
min="${new Date().toISOString().split('T')[0]}">
</div>
${this.mode === 'detailed' ? `
<div class="form-group">
<label for="tourType-${this.getId()}">Тип тура:</label>
<select id="tourType-${this.getId()}">
<option value="">Любой</option>
<option value="city">Городской тур</option>
<option value="mountain">Горный поход</option>
<option value="fishing">Рыбалка</option>
</select>
</div>
<div class="form-group">
<label for="groupSize-${this.getId()}">Размер группы:</label>
<input type="number"
id="groupSize-${this.getId()}"
min="1"
max="20"
value="1">
</div>
` : ''}
<div class="form-group">
<button type="button"
id="checkButton-${this.getId()}"
class="check-button">
🔍 Проверить
</button>
</div>
</div>
</div>
<div class="checker-results" id="checkerResults-${this.getId()}" style="display: none;">
<div class="results-content"></div>
</div>
${this.showSuggestions ? `
<div class="checker-suggestions" id="checkerSuggestions-${this.getId()}" style="display: none;">
<h5>Альтернативные варианты:</h5>
<div class="suggestions-list"></div>
</div>
` : ''}
</div>
`;
this.injectStyles();
}
getId() {
if (!this._id) {
this._id = 'availability-checker-' + Math.random().toString(36).substr(2, 9);
}
return this._id;
}
injectStyles() {
if (document.getElementById('availability-checker-styles')) return;
const styles = `
<style id="availability-checker-styles">
.availability-checker {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.checker-header {
margin-bottom: 20px;
padding: 15px;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border-radius: 8px;
text-align: center;
}
.checker-form {
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
}
.form-row {
display: flex;
gap: 15px;
align-items: end;
flex-wrap: wrap;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 120px;
}
.form-group label {
font-weight: 600;
font-size: 14px;
color: #495057;
}
.form-group input,
.form-group select {
padding: 10px 12px;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.check-button {
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
}
.results-summary {
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
}
.results-summary.available {
background: #d4edda;
color: #155724;
}
.results-summary.unavailable {
background: #f8d7da;
color: #721c24;
}
.available-guide {
padding: 12px;
background: white;
border: 1px solid #28a745;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.guide-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.guide-name {
font-weight: 600;
color: #2c3e50;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
async loadData() {
try {
const [guidesRes, holidaysRes, bookingsRes] = await Promise.all([
fetch('/api/guides'),
fetch('/api/holidays'),
fetch('/api/bookings')
]);
const guidesData = await guidesRes.json();
const holidaysData = await holidaysRes.json();
const bookingsData = await bookingsRes.json();
this.guides = Array.isArray(guidesData) ? guidesData : (guidesData.data || []);
this.holidays = Array.isArray(holidaysData) ? holidaysData : (holidaysData.data || []);
this.bookings = Array.isArray(bookingsData) ? bookingsData : (bookingsData.data || []);
} catch (error) {
console.error('Ошибка загрузки данных:', error);
}
}
bindEvents() {
const checkButton = this.container.querySelector(`#checkButton-${this.getId()}`);
checkButton.addEventListener('click', () => this.checkAvailability());
}
async checkAvailability() {
const dateInput = this.container.querySelector(`#checkDate-${this.getId()}`);
const date = dateInput.value;
if (!date) {
alert('Выберите дату');
return;
}
const availableGuides = this.getAvailableGuides(date);
const resultsContainer = this.container.querySelector(`#checkerResults-${this.getId()}`);
const resultsContent = resultsContainer.querySelector('.results-content');
if (availableGuides.length === 0) {
resultsContent.innerHTML = `
<div class="results-summary unavailable">
<span>❌</span>
<div>Нет доступных гидов на выбранную дату</div>
</div>
`;
} else {
resultsContent.innerHTML = `
<div class="results-summary available">
<span>✅</span>
<div>Доступно ${availableGuides.length} гидов</div>
</div>
${availableGuides.map(guide => `
<div class="available-guide">
<div class="guide-info">
<div class="guide-name">${guide.name}</div>
<div>${guide.specialization || 'Универсальный'}</div>
</div>
<div>${guide.hourly_rate ? guide.hourly_rate + '₩/час' : 'По договоренности'}</div>
</div>
`).join('')}
`;
}
resultsContainer.style.display = 'block';
}
getAvailableGuides(date) {
return this.guides.filter(guide => {
const holiday = this.holidays.find(h => h.guide_id === guide.id && h.holiday_date === date);
if (holiday) return false;
const booking = this.bookings.find(b =>
b.guide_id === guide.id &&
new Date(b.preferred_date).toISOString().split('T')[0] === date
);
if (booking) return false;
return true;
});
}
getId() {
if (!this._id) {
this._id = 'checker-' + Math.random().toString(36).substr(2, 9);
}
return this._id;
}
}
if (typeof window !== 'undefined') {
window.AvailabilityChecker = AvailabilityChecker;
}