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

@@ -230,19 +230,14 @@
}
}
// Проверяем по имени поля или тексту label
return fieldName.includes('image') ||
fieldName.includes('photo') ||
fieldName.includes('avatar') ||
fieldName.includes('picture') ||
fieldName.includes('banner') ||
fieldName.includes('thumbnail') ||
(fieldName.includes('url') && (labelText.includes('image') || labelText.includes('изображение'))) ||
labelText.includes('изображение') ||
labelText.includes('картинка') ||
labelText.includes('фото') ||
labelText.includes('image') ||
labelText.includes('picture');
// Проверяем по имени поля и содержанию
const isImageByName = fieldName.includes('image') && !fieldName.includes('title') && !fieldName.includes('alt');
const isImageByLabel = labelText.includes('image') || labelText.includes('изображение') || labelText.includes('фото');
const isImageUrlField = fieldName.includes('image_url') || fieldName === 'image_url';
console.log(`🔍 Проверка поля "${fieldName}": isImageByName=${isImageByName}, isImageByLabel=${isImageByLabel}, isImageUrlField=${isImageUrlField}`);
return isImageUrlField || isImageByName || isImageByLabel;
}
// Функция сканирования и добавления кнопок к полям изображений

View File

@@ -32,7 +32,101 @@ document.addEventListener('DOMContentLoaded', function() {
});
// ==========================================
// Поиск по сайту
// Инициализация компонентов бронирования
// ==========================================
// Компонент для проверки доступности на главной странице
const availabilityContainer = document.getElementById('availability-checker-container');
const guideSelectorContainer = document.getElementById('guide-selector-container');
if (availabilityContainer) {
const availabilityChecker = new AvailabilityChecker({
container: availabilityContainer,
mode: 'detailed',
showSuggestions: true,
onAvailabilityCheck: function(result) {
if (result.availableGuides && result.availableGuides.length > 0) {
// Показать селектор гидов если есть доступные
if (guideSelectorContainer) {
guideSelectorContainer.style.display = 'block';
const guideSelector = new GuideSelector({
container: guideSelectorContainer,
mode: 'booking',
showAvailability: true,
selectedDate: result.date,
onGuideSelect: function(guide) {
// Перейти к бронированию с выбранным гидом
window.location.href = `/routes?guide=${guide.id}&date=${result.date}`;
}
});
}
} else {
if (guideSelectorContainer) {
guideSelectorContainer.style.display = 'none';
}
}
}
});
}
// Календарь гидов на странице гидов
const guidesCalendarContainer = document.getElementById('guides-calendar-container');
if (guidesCalendarContainer) {
const guidesCalendar = new GuideCalendarWidget({
container: guidesCalendarContainer,
mode: 'readonly',
showControls: false,
showGuideInfo: true
});
}
// Компоненты бронирования на странице маршрута
const bookingAvailabilityContainer = document.getElementById('booking-availability-checker');
const bookingGuideSelectorContainer = document.getElementById('booking-guide-selector');
if (bookingAvailabilityContainer) {
const bookingAvailabilityChecker = new AvailabilityChecker({
container: bookingAvailabilityContainer,
mode: 'inline',
showSuggestions: false,
onAvailabilityCheck: function(result) {
if (result.availableGuides && result.availableGuides.length > 0) {
if (bookingGuideSelectorContainer) {
bookingGuideSelectorContainer.style.display = 'block';
const bookingGuideSelector = new GuideSelector({
container: bookingGuideSelectorContainer,
mode: 'booking',
showAvailability: false,
availableGuides: result.availableGuides,
onGuideSelect: function(guide) {
// Заполнить скрытое поле с ID гида
const selectedGuideIdInput = document.getElementById('selectedGuideId');
const preferredDateInput = document.getElementById('preferred_date');
const submitBtn = document.getElementById('submitBookingBtn');
if (selectedGuideIdInput) {
selectedGuideIdInput.value = guide.id;
}
if (preferredDateInput) {
preferredDateInput.value = result.date;
}
if (submitBtn) {
submitBtn.disabled = false;
}
}
});
}
}
}
});
}
// ==========================================
// Поиск по сайту (обновленная версия)
// ==========================================
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
@@ -378,6 +472,101 @@ document.addEventListener('DOMContentLoaded', function() {
}, 5000);
}
// ==========================================
// Вспомогательные функции для компонентов
// ==========================================
// Очистка результатов поиска
function clearSearchResults() {
const resultsContainer = document.getElementById('searchResults');
if (resultsContainer) {
resultsContainer.style.display = 'none';
}
const guideSelectorContainer = document.getElementById('guide-selector-container');
if (guideSelectorContainer) {
guideSelectorContainer.style.display = 'none';
}
}
// Функция для быстрого бронирования (вызывается из компонентов)
function quickBookTour(routeId, guideId, date, peopleCount = 1) {
// Создаем модальное окно для быстрого бронирования
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = `
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Бронирование тура</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="quickBookingForm" action="/bookings" method="POST">
<input type="hidden" name="route_id" value="${routeId}">
<input type="hidden" name="guide_id" value="${guideId}">
<input type="hidden" name="preferred_date" value="${date}">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Ваше имя *</label>
<input type="text" class="form-control" name="customer_name" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Количество человек</label>
<input type="number" class="form-control" name="people_count" value="${peopleCount}" min="1" max="20" required>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Email *</label>
<input type="email" class="form-control" name="customer_email" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Телефон *</label>
<input type="tel" class="form-control" name="customer_phone" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Особые пожелания</label>
<textarea class="form-control" name="special_requirements" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="submit" form="quickBookingForm" class="btn btn-primary">
<i class="fas fa-credit-card me-1"></i>Забронировать
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const bootstrapModal = new bootstrap.Modal(modal);
bootstrapModal.show();
// Удаление модального окна после закрытия
modal.addEventListener('hidden.bs.modal', function() {
document.body.removeChild(modal);
});
}
// Делаем функции доступными глобально для использования в компонентах
window.clearSearchResults = clearSearchResults;
window.quickBookTour = quickBookTour;
// ==========================================
// Утилитарные функции (продолжение)
// ==========================================
// ==========================================
// Финальные утилитарные функции
// ==========================================
function createAlertContainer() {
const container = document.createElement('div');
container.id = 'alert-container';
@@ -387,5 +576,13 @@ document.addEventListener('DOMContentLoaded', function() {
return container;
}
console.log('Korea Tourism Agency - JavaScript loaded successfully! 🇰🇷');
// Функция для форматирования чисел (валюта)
function formatNumber(num) {
return new Intl.NumberFormat('ru-RU').format(num);
}
// Делаем утилитарные функции доступными глобально
window.formatNumber = formatNumber;
console.log('Korea Tourism Agency - JavaScript with components loaded successfully! 🇰🇷');
});

View File

@@ -0,0 +1,477 @@
/**
* Универсальная интеграция медиа-менеджера в AdminJS
* Заменяет все стандартные диалоги выбора файлов на медиа-менеджер
*/
(function() {
'use strict';
console.log('🚀 Загружается универсальный медиа-менеджер для AdminJS...');
let mediaManagerModal = null;
let currentCallback = null;
// Создание модального окна медиа-менеджера
function createMediaManagerModal() {
if (mediaManagerModal) return mediaManagerModal;
const modal = document.createElement('div');
modal.className = 'universal-media-modal';
modal.innerHTML = `
<div class="universal-media-overlay"></div>
<div class="universal-media-content">
<div class="universal-media-header">
<h3>📁 Выбор изображения</h3>
<button class="universal-media-close">×</button>
</div>
<iframe class="universal-media-frame" src="/universal-media-manager.html"></iframe>
</div>
`;
// CSS стили
const style = document.createElement('style');
style.textContent = `
.universal-media-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
}
.universal-media-modal.active {
display: flex;
}
.universal-media-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
}
.universal-media-content {
position: relative;
width: 90vw;
height: 90vh;
max-width: 1200px;
max-height: 800px;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.universal-media-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.universal-media-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.universal-media-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s ease;
}
.universal-media-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.universal-media-frame {
width: 100%;
height: calc(100% - 60px);
border: none;
}
/* Стили для кнопок медиа-менеджера */
.media-manager-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
margin: 5px;
}
.media-manager-btn:hover {
background: #0056b3;
transform: translateY(-1px);
}
.media-manager-btn.small {
padding: 4px 8px;
font-size: 12px;
}
/* Скрываем стандартные input[type="file"] */
.media-replaced input[type="file"] {
display: none !important;
}
/* Стили для preview изображений */
.media-preview {
max-width: 200px;
max-height: 150px;
border-radius: 6px;
margin: 10px 0;
border: 2px solid #e9ecef;
object-fit: cover;
}
.media-preview.selected {
border-color: #28a745;
}
`;
if (!document.querySelector('#universal-media-styles')) {
style.id = 'universal-media-styles';
document.head.appendChild(style);
}
// События
const closeBtn = modal.querySelector('.universal-media-close');
const overlay = modal.querySelector('.universal-media-overlay');
closeBtn.addEventListener('click', closeMediaManager);
overlay.addEventListener('click', closeMediaManager);
document.body.appendChild(modal);
mediaManagerModal = modal;
return modal;
}
// Открытие медиа-менеджера
function openMediaManager(callback, options = {}) {
const modal = createMediaManagerModal();
currentCallback = callback;
// Обновляем заголовок если нужно
const header = modal.querySelector('.universal-media-header h3');
header.textContent = options.title || '📁 Выбор изображения';
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
// Закрытие медиа-менеджера
function closeMediaManager() {
if (mediaManagerModal) {
mediaManagerModal.classList.remove('active');
document.body.style.overflow = '';
currentCallback = null;
}
}
// Обработка сообщений от медиа-менеджера
window.addEventListener('message', function(event) {
if (event.data.type === 'media-manager-selection' && currentCallback) {
const files = event.data.files;
if (files && files.length > 0) {
currentCallback(files);
closeMediaManager();
}
}
});
// Замена стандартных input[type="file"] на медиа-менеджер
function replaceFileInputs() {
const fileInputs = document.querySelectorAll('input[type="file"]:not(.media-replaced)');
fileInputs.forEach(input => {
if (input.accept && !input.accept.includes('image')) {
return; // Пропускаем не-изображения
}
input.classList.add('media-replaced');
// Создаем кнопку медиа-менеджера
const button = document.createElement('button');
button.type = 'button';
button.className = 'media-manager-btn';
button.innerHTML = '📷 Выбрать изображение';
// Добавляем preview
const preview = document.createElement('img');
preview.className = 'media-preview';
preview.style.display = 'none';
// Добавляем скрытый input для хранения пути
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = input.name;
hiddenInput.value = input.value || '';
// Событие клика
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
openMediaManager((files) => {
const file = files[0];
// Обновляем значения
hiddenInput.value = file.url;
input.value = file.url;
// Показываем preview
preview.src = file.url;
preview.style.display = 'block';
preview.alt = file.name;
// Обновляем кнопку
button.innerHTML = '✏️ Заменить изображение';
// Добавляем кнопку удаления
if (!button.nextElementSibling?.classList.contains('media-remove-btn')) {
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'media-manager-btn small';
removeBtn.style.background = '#dc3545';
removeBtn.innerHTML = '🗑️ Удалить';
removeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Очищаем значения
hiddenInput.value = '';
input.value = '';
// Скрываем preview
preview.style.display = 'none';
// Восстанавливаем кнопку
button.innerHTML = '📷 Выбрать изображение';
removeBtn.remove();
});
button.parentElement.insertBefore(removeBtn, button.nextSibling);
}
// Вызываем событие change для совместимости
const changeEvent = new Event('change', { bubbles: true });
input.dispatchEvent(changeEvent);
}, {
title: input.dataset.title || 'Выбор изображения'
});
});
// Вставляем элементы
input.parentElement.insertBefore(button, input.nextSibling);
input.parentElement.insertBefore(preview, button.nextSibling);
input.parentElement.insertBefore(hiddenInput, preview.nextSibling);
// Если есть начальное значение, показываем preview
if (input.value) {
preview.src = input.value;
preview.style.display = 'block';
button.innerHTML = '✏️ Заменить изображение';
hiddenInput.value = input.value;
}
});
}
// Замена кнопок "Browse" в формах AdminJS
function replaceAdminJSBrowseButtons() {
// Ищем кнопки загрузки файлов AdminJS
const browseButtons = document.querySelectorAll('button[type="button"]:not(.media-replaced)');
browseButtons.forEach(button => {
const buttonText = button.textContent.toLowerCase();
if (buttonText.includes('browse') ||
buttonText.includes('выбрать') ||
buttonText.includes('загрузить') ||
buttonText.includes('upload')) {
button.classList.add('media-replaced');
// Заменяем обработчик клика
const originalHandler = button.onclick;
button.onclick = null;
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
openMediaManager((files) => {
const file = files[0];
// Ищем соответствующий input
const container = button.closest('.form-group, .field, .input-group');
const input = container?.querySelector('input[type="text"], input[type="url"], input[type="hidden"]');
if (input) {
input.value = file.url;
// Вызываем событие change
const changeEvent = new Event('change', { bubbles: true });
input.dispatchEvent(changeEvent);
// Обновляем preview если есть
const preview = container.querySelector('img');
if (preview) {
preview.src = file.url;
}
}
});
});
// Обновляем текст кнопки
button.innerHTML = '📷 Медиа-менеджер';
}
});
}
// Интеграция с полями изображений AdminJS
function integrateWithAdminJSImageFields() {
// Ищем поля с атрибутом accept="image/*"
const imageFields = document.querySelectorAll('input[accept*="image"]:not(.media-replaced)');
imageFields.forEach(field => {
field.classList.add('media-replaced');
const container = field.closest('.form-group, .field');
if (!container) return;
// Создаем кнопку медиа-менеджера
const mediaBtn = document.createElement('button');
mediaBtn.type = 'button';
mediaBtn.className = 'media-manager-btn';
mediaBtn.innerHTML = '📷 Открыть медиа-менеджер';
mediaBtn.addEventListener('click', (e) => {
e.preventDefault();
openMediaManager((files) => {
const file = files[0];
// Обновляем поле
field.value = file.url;
// Создаем событие change
const event = new Event('change', { bubbles: true });
field.dispatchEvent(event);
// Если есть label, обновляем его
const label = container.querySelector('label');
if (label && !label.querySelector('.selected-file')) {
const selectedSpan = document.createElement('span');
selectedSpan.className = 'selected-file';
selectedSpan.style.cssText = 'color: #28a745; font-weight: 500; margin-left: 10px;';
selectedSpan.textContent = `${file.name}`;
label.appendChild(selectedSpan);
}
});
});
// Вставляем кнопку после поля
field.parentElement.insertBefore(mediaBtn, field.nextSibling);
});
}
// Основная функция инициализации
function initMediaManager() {
console.log('🔧 Инициализация медиа-менеджера...');
// Замена различных типов полей
replaceFileInputs();
replaceAdminJSBrowseButtons();
integrateWithAdminJSImageFields();
console.log('✅ Медиа-менеджер инициализирован');
}
// Наблюдатель за изменениями DOM
const observer = new MutationObserver((mutations) => {
let shouldReinit = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // Element node
if (node.querySelector && (
node.querySelector('input[type="file"]') ||
node.querySelector('input[accept*="image"]') ||
node.querySelector('button[type="button"]')
)) {
shouldReinit = true;
}
}
});
}
});
if (shouldReinit) {
setTimeout(initMediaManager, 100);
}
});
// Запуск
function start() {
// Ждем загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMediaManager);
} else {
initMediaManager();
}
// Запуск наблюдателя
observer.observe(document.body, {
childList: true,
subtree: true
});
// Переинициализация при изменениях в SPA
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(initMediaManager, 500);
}
}, 1000);
}
// Глобальная функция для ручного открытия медиа-менеджера
window.openUniversalMediaManager = function(callback, options) {
openMediaManager(callback, options);
};
// Запуск
start();
})();