- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
306 lines
11 KiB
JavaScript
306 lines
11 KiB
JavaScript
/**
|
|
* 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;
|
|
} |