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