Продвинутые примеры

Комплексные примеры использования MNRFY Framework для решения реальных задач

1. Многоуровневое меню с правами доступа

Структура данных

<!-- Получение меню с проверкой прав -->
{% php %}
$menuItems = [
    [
        'title' => 'Главная',
        'url' => '/',
        'icon' => 'home',
        'permission' => null
    ],
    [
        'title' => 'Пользователи',
        'icon' => 'users',
        'permission' => 'users.view',
        'children' => [
            [
                'title' => 'Список пользователей',
                'url' => '/admin/users',
                'permission' => 'users.list'
            ],
            [
                'title' => 'Создать пользователя',
                'url' => '/admin/users/create',
                'permission' => 'users.create'
            ],
            [
                'title' => 'Роли и права',
                'url' => '/admin/roles',
                'permission' => 'roles.manage'
            ]
        ]
    ],
    [
        'title' => 'Контент',
        'icon' => 'newspaper',
        'permission' => 'content.view',
        'children' => [
            [
                'title' => 'Статьи',
                'url' => '/admin/articles',
                'permission' => 'articles.list',
                'badge' => db('1752665380840')->countItems('articles', ['status' => 'draft'])
            ],
            [
                'title' => 'Категории',
                'url' => '/admin/categories',
                'permission' => 'categories.manage'
            ],
            [
                'title' => 'Комментарии',
                'url' => '/admin/comments',
                'permission' => 'comments.moderate',
                'badge' => db('1752665380840')->countItems('comments', ['status' => 'pending'])
            ]
        ]
    ],
    [
        'title' => 'Система',
        'icon' => 'cogs',
        'permission' => 'system.view',
        'children' => [
            [
                'title' => 'Настройки',
                'url' => '/admin/settings',
                'permission' => 'system.settings'
            ],
            [
                'title' => 'Логи',
                'url' => '/admin/logs',
                'permission' => 'system.logs'
            ],
            [
                'title' => 'Кеш',
                'url' => '/admin/cache',
                'permission' => 'system.cache'
            ]
        ]
    ]
];
{% endphp %}

Рекурсивный компонент меню

<!-- components/admin-menu.html -->
{% macro render_menu_item(item, level) %}
    {% if not item.permission or can(item.permission) %}
        <li class="menu-item level-{{ level }} {{ item.children ? 'has-children' : '' }}">
            {% if item.url %}
                <a href="{{ item.url }}" 
                   class="menu-link {{ is_current_path(item.url) ? 'active' : '' }}"
                   {% if item.children %}onclick="toggleSubmenu(this)"{% endif %}>
                    {% if item.icon %}
                        <i class="fas fa-{{ item.icon }}"></i>
                    {% endif %}
                    
                    <span class="menu-text">{{ t('menu.' ~ item.title | lower | replace(' ', '_')) }}</span>
                    
                    {% if item.badge and item.badge > 0 %}
                        <span class="menu-badge">{{ item.badge }}</span>
                    {% endif %}
                    
                    {% if item.children %}
                        <i class="fas fa-chevron-down menu-toggle"></i>
                    {% endif %}
                </a>
            {% else %}
                <div class="menu-header" onclick="toggleSubmenu(this)">
                    {% if item.icon %}
                        <i class="fas fa-{{ item.icon }}"></i>
                    {% endif %}
                    <span>{{ t('menu.' ~ item.title | lower | replace(' ', '_')) }}</span>
                    <i class="fas fa-chevron-down menu-toggle"></i>
                </div>
            {% endif %}
            
            {% if item.children %}
                {% set visibleChildren = [] %}
                {% foreach item.children as child %}
                    {% if not child.permission or can(child.permission) %}
                        {% set visibleChildren = visibleChildren | push(child) %}
                    {% endif %}
                {% endforeach %}
                
                {% if visibleChildren | length > 0 %}
                    <ul class="submenu level-{{ level + 1 }}">
                        {% foreach visibleChildren as child %}
                            {{ _self.render_menu_item(child, level + 1) }}
                        {% endforeach %}
                    </ul>
                {% endif %}
            {% endif %}
        </li>
    {% endif %}
{% endmacro %}

<nav class="admin-sidebar">
    <div class="sidebar-header">
        <h3>{{ config('app.name') }}</h3>
        <p>Панель управления</p>
    </div>
    
    <ul class="main-menu">
        {% foreach menuItems as item %}
            {{ _self.render_menu_item(item, 0) }}
        {% endforeach %}
    </ul>
    
    <div class="sidebar-footer">
        <div class="user-info">
            <img src="{{ auth().avatar ?? asset('images/default-avatar.png') }}" alt="Avatar">
            <div>
                <strong>{{ auth().name }}</strong>
                <small>{{ t('roles.' ~ auth().role) }}</small>
            </div>
        </div>
    </div>
</nav>

<script>
function toggleSubmenu(element) {
    const submenu = element.nextElementSibling;
    const toggle = element.querySelector('.menu-toggle');
    
    if (submenu && submenu.classList.contains('submenu')) {
        submenu.classList.toggle('open');
        toggle.classList.toggle('rotated');
        element.classList.toggle('active');
    }
}
</script>

2. Продвинутая система фильтрации и поиска

Универсальная таблица с фильтрами

<!-- components/advanced-datatable.html -->
{% set currentFilters = request.all() %}
{% set sortField = request.input('sort', default_sort ?? 'created_at') %}
{% set sortDirection = request.input('direction', 'desc') %}
{% set searchQuery = request.input('search', '') %}
{% set perPage = request.input('per_page', 25) %}

<div class="advanced-datatable">
    <div class="datatable-controls">
        <!-- Поиск -->
        <div class="search-section">
            <div class="search-box">
                <input type="text" 
                       id="globalSearch" 
                       placeholder="{{ t('table.search_placeholder') }}"
                       value="{{ searchQuery }}"
                       onkeyup="debounceSearch(this.value)">
                <button type="button" onclick="clearSearch()">
                    <i class="fas fa-times"></i>
                </button>
            </div>
            
            <!-- Быстрые фильтры -->
            {% if quick_filters %}
                <div class="quick-filters">
                    {% foreach quick_filters as filter %}
                        <button type="button" 
                                class="filter-btn {{ currentFilters[filter.field] == filter.value ? 'active' : '' }}"
                                onclick="applyQuickFilter('{{ filter.field }}', '{{ filter.value }}')">
                            {% if filter.icon %}
                                <i class="fas fa-{{ filter.icon }}"></i>
                            {% endif %}
                            {{ filter.label }}
                            {% if filter.count %}
                                <span class="count">{{ filter.count }}</span>
                            {% endif %}
                        </button>
                    {% endforeach %}
                </div>
            {% endif %}
        </div>
        
        <!-- Расширенные фильтры -->
        <div class="advanced-filters" id="advancedFilters">
            <form method="GET" class="filters-form">
                <input type="hidden" name="search" value="{{ searchQuery }}">
                <input type="hidden" name="sort" value="{{ sortField }}">
                <input type="hidden" name="direction" value="{{ sortDirection }}">
                
                <div class="filters-grid">
                    {% foreach filters as filter %}
                        <div class="filter-group">
                            <label>{{ filter.label }}</label>
                            
                            {% if filter.type == 'select' %}
                                <select name="{{ filter.field }}">
                                    <option value="">{{ t('filter.all') }}</option>
                                    {% foreach filter.options as value, label %}
                                        <option value="{{ value }}" 
                                                {{ currentFilters[filter.field] == value ? 'selected' : '' }}>
                                            {{ label }}
                                        </option>
                                    {% endforeach %}
                                </select>
                            
                            {% elseif filter.type == 'date_range' %}
                                <div class="date-range">
                                    <input type="date" 
                                           name="{{ filter.field }}_from" 
                                           value="{{ currentFilters[filter.field ~ '_from'] }}"
                                           placeholder="От">
                                    <input type="date" 
                                           name="{{ filter.field }}_to" 
                                           value="{{ currentFilters[filter.field ~ '_to'] }}"
                                           placeholder="До">
                                </div>
                            
                            {% elseif filter.type == 'number_range' %}
                                <div class="number-range">
                                    <input type="number" 
                                           name="{{ filter.field }}_min" 
                                           value="{{ currentFilters[filter.field ~ '_min'] }}"
                                           placeholder="Мин.">
                                    <input type="number" 
                                           name="{{ filter.field }}_max" 
                                           value="{{ currentFilters[filter.field ~ '_max'] }}"
                                           placeholder="Макс.">
                                </div>
                            
                            {% elseif filter.type == 'multiselect' %}
                                <div class="multiselect" data-field="{{ filter.field }}">
                                    {% set selectedValues = currentFilters[filter.field] | split(',') %}
                                    {% foreach filter.options as value, label %}
                                        <label class="checkbox-label">
                                            <input type="checkbox" 
                                                   name="{{ filter.field }}[]" 
                                                   value="{{ value }}"
                                                   {{ value in selectedValues ? 'checked' : '' }}>
                                            {{ label }}
                                        </label>
                                    {% endforeach %}
                                </div>
                            {% endif %}
                        </div>
                    {% endforeach %}
                </div>
                
                <div class="filters-actions">
                    <button type="submit" class="btn btn-primary">
                        <i class="fas fa-filter"></i> {{ t('button.apply_filters') }}
                    </button>
                    <button type="button" class="btn btn-secondary" onclick="clearAllFilters()">
                        <i class="fas fa-times"></i> {{ t('button.clear_filters') }}
                    </button>
                    <button type="button" class="btn btn-info" onclick="saveFilterPreset()">
                        <i class="fas fa-bookmark"></i> {{ t('button.save_preset') }}
                    </button>
                </div>
            </form>
        </div>
        
        <!-- Панель управления -->
        <div class="table-toolbar">
            <div class="toolbar-left">
                <button type="button" class="btn btn-secondary" onclick="toggleFilters()">
                    <i class="fas fa-sliders-h"></i> {{ t('table.advanced_filters') }}
                </button>
                
                <div class="view-modes">
                    <button type="button" class="btn {{ view_mode == 'table' ? 'active' : '' }}" 
                            onclick="changeViewMode('table')">
                        <i class="fas fa-table"></i>
                    </button>
                    <button type="button" class="btn {{ view_mode == 'cards' ? 'active' : '' }}" 
                            onclick="changeViewMode('cards')">
                        <i class="fas fa-th"></i>
                    </button>
                    <button type="button" class="btn {{ view_mode == 'list' ? 'active' : '' }}" 
                            onclick="changeViewMode('list')">
                        <i class="fas fa-list"></i>
                    </button>
                </div>
            </div>
            
            <div class="toolbar-right">
                <!-- Количество на странице -->
                <select name="per_page" onchange="changePerPage(this.value)" class="per-page-select">
                    {% for count in [10, 25, 50, 100] %}
                        <option value="{{ count }}" {{ perPage == count ? 'selected' : '' }}>
                            {{ count }} / {{ t('table.page') }}
                        </option>
                    {% endfor %}
                </select>
                
                <!-- Экспорт -->
                <div class="dropdown">
                    <button type="button" class="btn btn-outline-primary dropdown-toggle">
                        <i class="fas fa-download"></i> {{ t('table.export') }}
                    </button>
                    <div class="dropdown-menu">
                        <a href="#" onclick="exportData('csv')">
                            <i class="fas fa-file-csv"></i> CSV
                        </a>
                        <a href="#" onclick="exportData('excel')">
                            <i class="fas fa-file-excel"></i> Excel
                        </a>
                        <a href="#" onclick="exportData('pdf')">
                            <i class="fas fa-file-pdf"></i> PDF
                        </a>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Активные фильтры -->
    {% set activeFilters = [] %}
    {% foreach currentFilters as key, value %}
        {% if value and key not in ['page', 'sort', 'direction'] %}
            {% set activeFilters = activeFilters | push({'key': key, 'value': value}) %}
        {% endif %}
    {% endforeach %}
    
    {% if activeFilters | length > 0 %}
        <div class="active-filters">
            <span>{{ t('table.active_filters') }}:</span>
            {% foreach activeFilters as filter %}
                <span class="filter-tag">
                    {{ filter.key }}: {{ filter.value }}
                    <button type="button" onclick="removeFilter('{{ filter.key }}')">×</button>
                </span>
            {% endforeach %}
            <button type="button" class="clear-all-filters" onclick="clearAllFilters()">
                {{ t('table.clear_all') }}
            </button>
        </div>
    {% endif %}
</div>

3. Сложная система уведомлений в реальном времени

Компонент центра уведомлений

<!-- components/notification-center.html -->
{% set unreadCount = db('1752665380840')->countItems('notifications', {
    'user_id': auth().id,
    'read_at': null
}) %}

<div class="notification-center">
    <div class="notification-trigger" onclick="toggleNotifications()">
        <i class="fas fa-bell"></i>
        {% if unreadCount > 0 %}
            <span class="notification-badge">
                {{ unreadCount > 99 ? '99+' : unreadCount }}
            </span>
        {% endif %}
    </div>
    
    <div class="notification-dropdown" id="notificationDropdown">
        <div class="notification-header">
            <h3>{{ t('notifications.title') }}</h3>
            <div class="notification-actions">
                {% if unreadCount > 0 %}
                    <button type="button" onclick="markAllAsRead()" class="btn-link">
                        {{ t('notifications.mark_all_read') }}
                    </button>
                {% endif %}
                <button type="button" onclick="refreshNotifications()" class="btn-link">
                    <i class="fas fa-sync-alt"></i>
                </button>
            </div>
        </div>
        
        <div class="notification-filters">
            <button type="button" class="filter-btn active" data-filter="all">
                {{ t('notifications.all') }}
            </button>
            <button type="button" class="filter-btn" data-filter="unread">
                {{ t('notifications.unread') }} ({{ unreadCount }})
            </button>
            <button type="button" class="filter-btn" data-filter="system">
                {{ t('notifications.system') }}
            </button>
            <button type="button" class="filter-btn" data-filter="messages">
                {{ t('notifications.messages') }}
            </button>
        </div>
        
        <div class="notification-list" id="notificationList">
            {% set notifications = db('1752665380840')->getItems('notifications', {
                'user_id': auth().id
            }, ['*'], 'created_at', 'DESC', null, 50) %}
            
            {% if notifications | length > 0 %}
                {% foreach notifications as notification %}
                    {% component 'notification-item' with {'notification': notification} %}
                {% endforeach %}
            {% else %}
                <div class="no-notifications">
                    <i class="fas fa-bell-slash fa-2x"></i>
                    <p>{{ t('notifications.no_notifications') }}</p>
                </div>
            {% endif %}
        </div>
        
        <div class="notification-footer">
            <a href="/notifications" class="btn btn-primary btn-block">
                {{ t('notifications.view_all') }}
            </a>
        </div>
    </div>
</div>

<!-- WebSocket подключение для real-time уведомлений -->
<script>
class NotificationCenter {
    constructor() {
        this.websocket = null;
        this.reconnectInterval = 5000;
        this.maxReconnectAttempts = 5;
        this.reconnectAttempts = 0;
        
        this.init();
        this.connectWebSocket();
    }
    
    init() {
        // Инициализация обработчиков событий
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.notification-center')) {
                this.closeDropdown();
            }
        });
        
        // Фильтры уведомлений
        document.querySelectorAll('.filter-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                this.filterNotifications(e.target.dataset.filter);
            });
        });
        
        // Проверка уведомлений каждые 30 секунд (fallback)
        setInterval(() => {
            this.checkNotifications();
        }, 30000);
    }
    
    connectWebSocket() {
        try {
            const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
            const wsUrl = `${protocol}//${location.host}/ws/notifications`;
            
            this.websocket = new WebSocket(wsUrl);
            
            this.websocket.onopen = () => {
                console.log('Notification WebSocket connected');
                this.reconnectAttempts = 0;
                
                // Аутентификация через WebSocket
                this.websocket.send(JSON.stringify({
                    type: 'auth',
                    token: document.querySelector('[name="csrf-token"]').content
                }));
            };
            
            this.websocket.onmessage = (event) => {
                const data = JSON.parse(event.data);
                this.handleNotification(data);
            };
            
            this.websocket.onclose = () => {
                console.log('Notification WebSocket disconnected');
                this.reconnect();
            };
            
            this.websocket.onerror = (error) => {
                console.error('WebSocket error:', error);
            };
            
        } catch (error) {
            console.error('Failed to connect WebSocket:', error);
            this.reconnect();
        }
    }
    
    reconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            setTimeout(() => {
                this.connectWebSocket();
            }, this.reconnectInterval);
        }
    }
    
    handleNotification(data) {
        switch(data.type) {
            case 'new_notification':
                this.addNotification(data.notification);
                this.updateBadge();
                this.showToast(data.notification);
                break;
            
            case 'notification_read':
                this.markAsRead(data.notificationId);
                this.updateBadge();
                break;
            
            case 'bulk_read':
                this.markAllAsRead();
                break;
        }
    }
    
    addNotification(notification) {
        const list = document.getElementById('notificationList');
        const item = this.createNotificationItem(notification);
        list.insertBefore(item, list.firstChild);
        
        // Анимация появления
        item.classList.add('notification-new');
        setTimeout(() => {
            item.classList.remove('notification-new');
        }, 3000);
    }
    
    createNotificationItem(notification) {
        const div = document.createElement('div');
        div.className = `notification-item ${notification.read_at ? '' : 'unread'} ${notification.type}`;
        div.dataset.id = notification.id;
        
        const timeAgo = this.timeAgo(new Date(notification.created_at * 1000));
        
        div.innerHTML = `
            <div class="notification-icon">
                <i class="fas fa-${this.getNotificationIcon(notification.type)}"></i>
            </div>
            <div class="notification-content">
                <div class="notification-title">${notification.title}</div>
                <div class="notification-message">${notification.message}</div>
                <div class="notification-time">${timeAgo}</div>
            </div>
            <div class="notification-actions">
                ${notification.action_url ? `<a href="${notification.action_url}" class="btn-link">Просмотр</a>` : ''}
                <button type="button" onclick="markAsRead(${notification.id})" class="btn-link">
                    <i class="fas fa-check"></i>
                </button>
            </div>
        `;
        
        return div;
    }
    
    showToast(notification) {
        // Создание toast уведомления
        const toast = document.createElement('div');
        toast.className = 'notification-toast';
        toast.innerHTML = `
            <div class="toast-icon">
                <i class="fas fa-${this.getNotificationIcon(notification.type)}"></i>
            </div>
            <div class="toast-content">
                <strong>${notification.title}</strong>
                <p>${notification.message}</p>
            </div>
            <button type="button" class="toast-close" onclick="this.parentElement.remove()">
                ×
            </button>
        `;
        
        document.body.appendChild(toast);
        
        // Автоматическое скрытие через 5 секунд
        setTimeout(() => {
            if (toast.parentElement) {
                toast.remove();
            }
        }, 5000);
    }
    
    getNotificationIcon(type) {
        const icons = {
            'system': 'cog',
            'message': 'envelope',
            'order': 'shopping-cart',
            'payment': 'credit-card',
            'security': 'shield-alt',
            'promotion': 'gift'
        };
        return icons[type] || 'bell';
    }
    
    timeAgo(date) {
        const seconds = Math.floor((new Date() - date) / 1000);
        
        if (seconds < 60) return 'только что';
        if (seconds < 3600) return Math.floor(seconds / 60) + ' мин. назад';
        if (seconds < 86400) return Math.floor(seconds / 3600) + ' ч. назад';
        if (seconds < 2592000) return Math.floor(seconds / 86400) + ' дн. назад';
        
        return date.toLocaleDateString();
    }
    
    async updateBadge() {
        try {
            const response = await fetch('/api/notifications/unread-count');
            const data = await response.json();
            
            const badge = document.querySelector('.notification-badge');
            if (data.count > 0) {
                if (!badge) {
                    const newBadge = document.createElement('span');
                    newBadge.className = 'notification-badge';
                    document.querySelector('.notification-trigger').appendChild(newBadge);
                }
                document.querySelector('.notification-badge').textContent = 
                    data.count > 99 ? '99+' : data.count;
            } else if (badge) {
                badge.remove();
            }
        } catch (error) {
            console.error('Failed to update badge:', error);
        }
    }
}

// Инициализация
const notificationCenter = new NotificationCenter();

function toggleNotifications() {
    const dropdown = document.getElementById('notificationDropdown');
    dropdown.classList.toggle('show');
    
    if (dropdown.classList.contains('show')) {
        notificationCenter.checkNotifications();
    }
}

async function markAsRead(notificationId) {
    try {
        const response = await fetch(`/api/notifications/${notificationId}/read`, {
            method: 'POST',
            headers: {
                'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content,
                'Content-Type': 'application/json'
            }
        });
        
        if (response.ok) {
            const item = document.querySelector(`[data-id="${notificationId}"]`);
            if (item) {
                item.classList.remove('unread');
            }
            notificationCenter.updateBadge();
        }
    } catch (error) {
        console.error('Failed to mark as read:', error);
    }
}

async function markAllAsRead() {
    try {
        const response = await fetch('/api/notifications/mark-all-read', {
            method: 'POST',
            headers: {
                'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
            }
        });
        
        if (response.ok) {
            document.querySelectorAll('.notification-item').forEach(item => {
                item.classList.remove('unread');
            });
            notificationCenter.updateBadge();
        }
    } catch (error) {
        console.error('Failed to mark all as read:', error);
    }
}
</script>

4. Система управления файлами с drag&drop

Файловый менеджер

<!-- components/file-manager.html -->
<div class="file-manager" id="fileManager">
    <div class="file-manager-toolbar">
        <div class="toolbar-left">
            <div class="breadcrumb">
                {% set pathParts = current_path | split('/') | filter %}
                <a href="#" onclick="navigateToPath('/')" class="breadcrumb-item">
                    <i class="fas fa-home"></i>
                </a>
                {% set buildPath = '' %}
                {% foreach pathParts as part %}
                    {% set buildPath = buildPath ~ '/' ~ part %}
                    <span class="breadcrumb-separator">/</span>
                    <a href="#" onclick="navigateToPath('{{ buildPath }}')" class="breadcrumb-item">
                        {{ part }}
                    </a>
                {% endforeach %}
            </div>
        </div>
        
        <div class="toolbar-right">
            <button type="button" class="btn btn-primary" onclick="openUploadModal()">
                <i class="fas fa-upload"></i> {{ t('file.upload') }}
            </button>
            <button type="button" class="btn btn-secondary" onclick="createFolder()">
                <i class="fas fa-folder-plus"></i> {{ t('file.new_folder') }}
            </button>
            
            <div class="view-toggle">
                <button type="button" class="btn {{ view_mode == 'grid' ? 'active' : '' }}" 
                        onclick="setViewMode('grid')">
                    <i class="fas fa-th"></i>
                </button>
                <button type="button" class="btn {{ view_mode == 'list' ? 'active' : '' }}" 
                        onclick="setViewMode('list')">
                    <i class="fas fa-list"></i>
                </button>
            </div>
        </div>
    </div>
    
    <!-- Файловая область с drag&drop -->
    <div class="file-area {{ view_mode }}-view" 
         id="fileArea"
         ondrop="handleDrop(event)" 
         ondragover="handleDragOver(event)"
         ondragenter="handleDragEnter(event)"
         ondragleave="handleDragLeave(event)">
        
        {% set files = getDirectoryContents(current_path) %}
        
        {% if files | length > 0 %}
            {% foreach files as file %}
                <div class="file-item {{ file.type }}" 
                     data-path="{{ file.path }}"
                     data-type="{{ file.type }}"
                     draggable="true"
                     ondragstart="handleFileDragStart(event)"
                     onclick="selectFile(this)"
                     ondblclick="openFile(this)">
                    
                    <div class="file-thumbnail">
                        {% if file.type == 'image' and file.thumbnail %}
                            <img src="{{ file.thumbnail }}" alt="{{ file.name }}" loading="lazy">
                        {% else %}
                            <i class="fas fa-{{ getFileIcon(file) }} file-icon"></i>
                        {% endif %}
                        
                        {% if file.type != 'folder' %}
                            <div class="file-overlay">
                                <button type="button" class="btn btn-sm btn-primary" 
                                        onclick="previewFile('{{ file.path }}')">
                                    <i class="fas fa-eye"></i>
                                </button>
                                <button type="button" class="btn btn-sm btn-danger" 
                                        onclick="deleteFile('{{ file.path }}')">
                                    <i class="fas fa-trash"></i>
                                </button>
                            </div>
                        {% endif %}
                    </div>
                    
                    <div class="file-info">
                        <div class="file-name" title="{{ file.name }}">
                            {{ file.name | truncate(20) }}
                        </div>
                        {% if file.type != 'folder' %}
                            <div class="file-size">{{ file.size | filesize }}</div>
                            <div class="file-date">{{ file.modified | date('d.m.Y') }}</div>
                        {% endif %}
                    </div>
                    
                    <div class="file-actions">
                        <div class="dropdown">
                            <button type="button" class="btn btn-sm" onclick="toggleFileMenu(this)">
                                <i class="fas fa-ellipsis-v"></i>
                            </button>
                            <div class="dropdown-menu">
                                {% if file.type == 'folder' %}
                                    <a href="#" onclick="openFolder('{{ file.path }}')">
                                        <i class="fas fa-folder-open"></i> {{ t('file.open') }}
                                    </a>
                                {% else %}
                                    <a href="{{ file.download_url }}" download>
                                        <i class="fas fa-download"></i> {{ t('file.download') }}
                                    </a>
                                    <a href="#" onclick="copyFileUrl('{{ file.public_url }}')">
                                        <i class="fas fa-link"></i> {{ t('file.copy_link') }}
                                    </a>
                                {% endif %}
                                <a href="#" onclick="renameFile('{{ file.path }}', '{{ file.name }}')">
                                    <i class="fas fa-edit"></i> {{ t('file.rename') }}
                                </a>
                                <a href="#" onclick="deleteFile('{{ file.path }}')" class="text-danger">
                                    <i class="fas fa-trash"></i> {{ t('file.delete') }}
                                </a>
                            </div>
                        </div>
                    </div>
                </div>
            {% endforeach %}
        {% else %}
            <div class="empty-folder">
                <i class="fas fa-folder-open fa-4x"></i>
                <h3>{{ t('file.empty_folder') }}</h3>
                <p>{{ t('file.empty_folder_hint') }}</p>
            </div>
        {% endif %}
        
        <!-- Drag&Drop область -->
        <div class="drag-overlay" id="dragOverlay">
            <div class="drag-content">
                <i class="fas fa-cloud-upload-alt fa-4x"></i>
                <h3>{{ t('file.drop_files_here') }}</h3>
            </div>
        </div>
    </div>
    
    <!-- Информационная панель -->
    <div class="file-info-panel" id="fileInfoPanel">
        <h4>{{ t('file.properties') }}</h4>
        <div id="fileProperties">
            {{ t('file.select_file_for_info') }}
        </div>
    </div>
</div>

<!-- Модальное окно загрузки -->
<div class="modal" id="uploadModal">
    <div class="modal-content">
        <div class="modal-header">
            <h3>{{ t('file.upload_files') }}</h3>
            <button type="button" class="modal-close" onclick="closeUploadModal()">×</button>
        </div>
        
        <div class="modal-body">
            <div class="upload-area" id="uploadArea">
                <input type="file" id="fileInput" multiple accept="*/*" onchange="handleFileSelect(event)">
                <div class="upload-prompt">
                    <i class="fas fa-cloud-upload-alt fa-3x"></i>
                    <h4>{{ t('file.drag_or_click') }}</h4>
                    <p>{{ t('file.max_size_info', {'size': max_file_size}) }}</p>
                </div>
            </div>
            
            <div class="upload-queue" id="uploadQueue"></div>
        </div>
        
        <div class="modal-footer">
            <button type="button" class="btn btn-secondary" onclick="closeUploadModal()">
                {{ t('button.cancel') }}
            </button>
            <button type="button" class="btn btn-primary" onclick="startUpload()" id="uploadBtn" disabled>
                {{ t('file.start_upload') }}
            </button>
        </div>
    </div>
</div>

<script>
class FileManager {
    constructor() {
        this.currentPath = '/';
        this.selectedFiles = [];
        this.uploadQueue = [];
        this.dragCounter = 0;
        
        this.init();
    }
    
    init() {
        // Инициализация drag&drop для всего окна
        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
            document.addEventListener(eventName, this.preventDefaults, false);
        });
        
        ['dragenter', 'dragover'].forEach(eventName => {
            document.addEventListener(eventName, this.highlight.bind(this), false);
        });
        
        ['dragleave', 'drop'].forEach(eventName => {
            document.addEventListener(eventName, this.unhighlight.bind(this), false);
        });
        
        document.addEventListener('drop', this.handleGlobalDrop.bind(this), false);
    }
    
    preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
    }
    
    highlight() {
        this.dragCounter++;
        document.getElementById('dragOverlay').classList.add('show');
    }
    
    unhighlight() {
        this.dragCounter--;
        if (this.dragCounter === 0) {
            document.getElementById('dragOverlay').classList.remove('show');
        }
    }
    
    handleGlobalDrop(e) {
        this.dragCounter = 0;
        this.unhighlight();
        
        const files = e.dataTransfer.files;
        if (files.length > 0) {
            this.handleFiles(files);
        }
    }
    
    async handleFiles(files) {
        const fileArray = Array.from(files);
        
        for (const file of fileArray) {
            // Проверка размера файла
            if (file.size > {{ max_file_size_bytes }}) {
                this.showError(`Файл ${file.name} слишком большой. Максимум {{ max_file_size }}`);
                continue;
            }
            
            // Проверка типа файла
            if (!this.isAllowedFileType(file)) {
                this.showError(`Тип файла ${file.name} не разрешен`);
                continue;
            }
            
            await this.uploadFile(file);
        }
        
        // Обновляем файловый менеджер
        this.refreshCurrentDirectory();
    }
    
    async uploadFile(file) {
        const formData = new FormData();
        formData.append('file', file);
        formData.append('path', this.currentPath);
        formData.append('_token', document.querySelector('[name="csrf-token"]').content);
        
        try {
            // Создаем индикатор прогресса
            const progressId = this.createProgressIndicator(file.name);
            
            const response = await fetch('/api/files/upload', {
                method: 'POST',
                body: formData,
                headers: {
                    'X-Requested-With': 'XMLHttpRequest'
                }
            });
            
            const result = await response.json();
            
            if (result.success) {
                this.updateProgressIndicator(progressId, 100, 'Загружен');
                this.showSuccess(`Файл ${file.name} успешно загружен`);
                
                // Добавляем новый файл в интерфейс
                this.addFileToDisplay(result.file);
            } else {
                this.updateProgressIndicator(progressId, 0, 'Ошибка');
                this.showError(result.message || 'Ошибка загрузки файла');
            }
            
        } catch (error) {
            console.error('Upload error:', error);
            this.showError('Ошибка загрузки файла');
        }
    }
    
    createProgressIndicator(fileName) {
        const progressId = 'progress_' + Date.now();
        const progressHTML = `
            <div id="${progressId}" class="upload-progress">
                <div class="file-name">${fileName}</div>
                <div class="progress-bar">
                    <div class="progress-fill" style="width: 0%"></div>
                </div>
                <div class="progress-text">0%</div>
            </div>
        `;
        
        // Добавляем в очередь загрузки или в специальную область
        const container = document.getElementById('uploadQueue') || document.body;
        container.insertAdjacentHTML('beforeend', progressHTML);
        
        return progressId;
    }
    
    updateProgressIndicator(progressId, percent, status) {
        const progressElement = document.getElementById(progressId);
        if (progressElement) {
            const fill = progressElement.querySelector('.progress-fill');
            const text = progressElement.querySelector('.progress-text');
            
            fill.style.width = percent + '%';
            text.textContent = status || (percent + '%');
            
            if (percent === 100) {
                setTimeout(() => {
                    progressElement.remove();
                }, 2000);
            }
        }
    }
    
    isAllowedFileType(file) {
        const allowedTypes = {{ allowed_file_types | json }};
        const fileExtension = file.name.split('.').pop().toLowerCase();
        return allowedTypes.includes('*') || allowedTypes.includes(fileExtension);
    }
    
    showError(message) {
        this.showNotification(message, 'error');
    }
    
    showSuccess(message) {
        this.showNotification(message, 'success');
    }
    
    showNotification(message, type) {
        const notification = document.createElement('div');
        notification.className = `notification notification-${type}`;
        notification.textContent = message;
        
        document.body.appendChild(notification);
        
        setTimeout(() => {
            notification.classList.add('show');
        }, 100);
        
        setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => {
                notification.remove();
            }, 300);
        }, 5000);
    }
}

// Инициализация файлового менеджера
const fileManager = new FileManager();

// Глобальные функции для интерфейса
function navigateToPath(path) {
    fileManager.currentPath = path;
    window.location.href = `?path=${encodeURIComponent(path)}`;
}

function selectFile(element) {
    // Снимаем выделение с других файлов
    document.querySelectorAll('.file-item.selected').forEach(item => {
        item.classList.remove('selected');
    });
    
    // Выделяем текущий файл
    element.classList.add('selected');
    
    // Показываем информацию о файле
    showFileInfo(element);
}

function showFileInfo(fileElement) {
    const filePath = fileElement.dataset.path;
    const fileName = fileElement.querySelector('.file-name').textContent;
    const fileType = fileElement.dataset.type;
    
    // Загружаем детальную информацию о файле
    fetch(`/api/files/info?path=${encodeURIComponent(filePath)}`)
        .then(response => response.json())
        .then(data => {
            const infoPanel = document.getElementById('fileProperties');
            infoPanel.innerHTML = `
                <div class="property"><strong>Имя:</strong> ${data.name}</div>
                <div class="property"><strong>Размер:</strong> ${data.size}</div>
                <div class="property"><strong>Тип:</strong> ${data.type}</div>
                <div class="property"><strong>Изменен:</strong> ${data.modified}</div>
                ${data.dimensions ? `<div class="property"><strong>Размеры:</strong> ${data.dimensions}</div>` : ''}
            `;
        });
}

function deleteFile(filePath) {
    if (!confirm('{{ t("file.confirm_delete") }}')) return;
    
    fetch('/api/files/delete', {
        method: 'DELETE',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
        },
        body: JSON.stringify({ path: filePath })
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            // Удаляем элемент из интерфейса
            const fileElement = document.querySelector(`[data-path="${filePath}"]`);
            if (fileElement) {
                fileElement.remove();
            }
            fileManager.showSuccess('{{ t("file.deleted_successfully") }}');
        } else {
            fileManager.showError(data.message || '{{ t("file.delete_error") }}');
        }
    })
    .catch(error => {
        fileManager.showError('{{ t("file.delete_error") }}');
    });
}
</script>

5. Интерактивная аналитическая панель

Dashboard с графиками и метриками в реальном времени

<!-- pages/analytics-dashboard.html -->
{% extends 'layouts/admin.html' %}

{% block title %}Аналитика - {{ parent() }}{% endblock %}

{% block page_styles %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.css">
<link rel="stylesheet" href="{{ asset('css/dashboard.css') }}">
{% endblock %}

{% block content %}
<div class="analytics-dashboard">
    <!-- Панель фильтров -->
    <div class="dashboard-filters">
        <div class="filter-group">
            <label>Период:</label>
            <select id="periodFilter" onchange="updateDashboard()">
                <option value="today">Сегодня</option>
                <option value="week" selected>Неделя</option>
                <option value="month">Месяц</option>
                <option value="quarter">Квартал</option>
                <option value="year">Год</option>
                <option value="custom">Произвольный</option>
            </select>
        </div>
        
        <div class="filter-group date-range" id="customDateRange" style="display: none;">
            <input type="date" id="dateFrom" onchange="updateDashboard()">
            <span>—</span>
            <input type="date" id="dateTo" onchange="updateDashboard()">
        </div>
        
        <div class="filter-group">
            <label>Сравнение:</label>
            <select id="compareFilter" onchange="updateDashboard()">
                <option value="">Без сравнения</option>
                <option value="previous">С предыдущим периодом</option>
                <option value="year_ago">С прошлым годом</option>
            </select>
        </div>
        
        <div class="filter-actions">
            <button type="button" class="btn btn-outline-primary" onclick="exportReport()">
                <i class="fas fa-download"></i> Экспорт
            </button>
            <button type="button" class="btn btn-outline-secondary" onclick="scheduleReport()">
                <i class="fas fa-clock"></i> Отчет по расписанию
            </button>
        </div>
    </div>
    
    <!-- КПИ карточки -->
    <div class="kpi-grid" id="kpiGrid">
        {% set kpiMetrics = getKPIMetrics(period, compare_period) %}
        
        {% foreach kpiMetrics as metric %}
            <div class="kpi-card {{ metric.trend }}" data-metric="{{ metric.key }}">
                <div class="kpi-header">
                    <div class="kpi-icon">
                        <i class="fas fa-{{ metric.icon }}"></i>
                    </div>
                    <div class="kpi-actions">
                        <button type="button" class="btn-link" onclick="drillDown('{{ metric.key }}')">
                            <i class="fas fa-search-plus"></i>
                        </button>
                    </div>
                </div>
                
                <div class="kpi-content">
                    <div class="kpi-value">
                        {{ metric.formatted_value }}
                        <span class="kpi-unit">{{ metric.unit }}</span>
                    </div>
                    
                    <div class="kpi-title">{{ metric.title }}</div>
                    
                    {% if metric.change %}
                        <div class="kpi-change {{ metric.change > 0 ? 'positive' : (metric.change < 0 ? 'negative' : 'neutral') }}">
                            <i class="fas fa-{{ metric.change > 0 ? 'arrow-up' : (metric.change < 0 ? 'arrow-down' : 'minus') }}"></i>
                            {{ metric.change | abs | number(1) }}%
                            <span class="change-period">vs {{ metric.compare_period_name }}</span>
                        </div>
                    {% endif %}
                </div>
                
                <div class="kpi-sparkline">
                    <canvas id="sparkline_{{ metric.key }}" width="200" height="50"></canvas>
                </div>
            </div>
        {% endforeach %}
    </div>
    
    <!-- Основные графики -->
    <div class="charts-grid">
        <div class="chart-container large">
            <div class="chart-header">
                <h3>Динамика основных метрик</h3>
                <div class="chart-controls">
                    <div class="metric-toggles">
                        <label class="checkbox-label">
                            <input type="checkbox" checked data-metric="revenue">
                            <span class="metric-color" style="background: #3498db"></span>
                            Выручка
                        </label>
                        <label class="checkbox-label">
                            <input type="checkbox" checked data-metric="orders">
                            <span class="metric-color" style="background: #2ecc71"></span>
                            Заказы
                        </label>
                        <label class="checkbox-label">
                            <input type="checkbox" data-metric="users">
                            <span class="metric-color" style="background: #e74c3c"></span>
                            Пользователи
                        </label>
                    </div>
                </div>
            </div>
            <div class="chart-body">
                <canvas id="mainChart" width="800" height="400"></canvas>
            </div>
        </div>
        
        <div class="chart-container medium">
            <div class="chart-header">
                <h3>Топ товаров</h3>
            </div>
            <div class="chart-body">
                <canvas id="topProductsChart" width="400" height="400"></canvas>
            </div>
        </div>
        
        <div class="chart-container medium">
            <div class="chart-header">
                <h3>Источники трафика</h3>
            </div>
            <div class="chart-body">
                <canvas id="trafficSourcesChart" width="400" height="400"></canvas>
            </div>
        </div>
        
        <div class="chart-container large">
            <div class="chart-header">
                <h3>Аналитика по регионам</h3>
            </div>
            <div class="chart-body">
                <div id="regionMap" style="height: 400px;"></div>
            </div>
        </div>
    </div>
    
    <!-- Детальные таблицы -->
    <div class="detailed-tables">
        <div class="table-container">
            <div class="table-header">
                <h3>Последние заказы</h3>
                <a href="/admin/orders" class="btn btn-outline-primary btn-sm">
                    Все заказы <i class="fas fa-arrow-right"></i>
                </a>
            </div>
            
            {% set recentOrders = getRecentOrders(10) %}
            <div class="table-responsive">
                <table class="table">
                    <thead>
                        <tr>
                            <th>№ заказа</th>
                            <th>Клиент</th>
                            <th>Сумма</th>
                            <th>Статус</th>
                            <th>Дата</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% foreach recentOrders as order %}
                            <tr onclick="window.open('/admin/orders/{{ order.id }}', '_blank')" style="cursor: pointer;">
                                <td>#{{ order.number }}</td>
                                <td>
                                    <div class="customer-info">
                                        {% if order.customer.avatar %}
                                            <img src="{{ order.customer.avatar }}" alt="" class="avatar-sm">
                                        {% endif %}
                                        {{ order.customer.name }}
                                    </div>
                                </td>
                                <td>{{ order.total | money('₽') }}</td>
                                <td>
                                    <span class="status-badge {{ order.status }}">
                                        {{ t('order.status.' ~ order.status) }}
                                    </span>
                                </td>
                                <td>{{ order.created_at | time_ago }}</td>
                            </tr>
                        {% endforeach %}
                    </tbody>
                </table>
            </div>
        </div>
        
        <div class="table-container">
            <div class="table-header">
                <h3>Активность пользователей</h3>
            </div>
            
            {% set userActivity = getUserActivityStats() %}
            <div class="activity-list">
                {% foreach userActivity as activity %}
                    <div class="activity-item">
                        <div class="activity-avatar">
                            {% if activity.user.avatar %}
                                <img src="{{ activity.user.avatar }}" alt="">
                            {% else %}
                                <div class="avatar-placeholder">
                                    {{ activity.user.name | first | upper }}
                                </div>
                            {% endif %}
                        </div>
                        
                        <div class="activity-content">
                            <div class="activity-text">
                                <strong>{{ activity.user.name }}</strong>
                                {{ t('activity.' ~ activity.type, activity.data) }}
                            </div>
                            <div class="activity-time">
                                {{ activity.created_at | time_ago }}
                            </div>
                        </div>
                        
                        <div class="activity-icon">
                            <i class="fas fa-{{ getActivityIcon(activity.type) }}"></i>
                        </div>
                    </div>
                {% endforeach %}
            </div>
        </div>
    </div>
</div>

<!-- WebSocket для real-time обновлений -->
<script>
class AnalyticsDashboard {
    constructor() {
        this.charts = {};
        this.websocket = null;
        this.updateInterval = null;
        
        this.init();
        this.initCharts();
        this.connectWebSocket();
        this.startPeriodicUpdates();
    }
    
    init() {
        // Обработчик изменения фильтров
        document.getElementById('periodFilter').addEventListener('change', (e) => {
            const customRange = document.getElementById('customDateRange');
            if (e.target.value === 'custom') {
                customRange.style.display = 'flex';
            } else {
                customRange.style.display = 'none';
            }
        });
        
        // Обработчики toggle метрик
        document.querySelectorAll('[data-metric]').forEach(checkbox => {
            checkbox.addEventListener('change', (e) => {
                this.toggleMetric(e.target.dataset.metric, e.target.checked);
            });
        });
    }
    
    async initCharts() {
        // Основной график
        const mainCtx = document.getElementById('mainChart').getContext('2d');
        const chartData = await this.loadChartData('main');
        
        this.charts.main = new Chart(mainCtx, {
            type: 'line',
            data: chartData,
            options: {
                responsive: true,
                interaction: {
                    mode: 'index',
                    intersect: false,
                },
                plugins: {
                    legend: {
                        display: false
                    },
                    tooltip: {
                        callbacks: {
                            label: function(context) {
                                const label = context.dataset.label || '';
                                const value = context.parsed.y;
                                
                                if (label.includes('Выручка')) {
                                    return label + ': ' + new Intl.NumberFormat('ru-RU', {
                                        style: 'currency',
                                        currency: 'RUB'
                                    }).format(value);
                                }
                                
                                return label + ': ' + value.toLocaleString('ru-RU');
                            }
                        }
                    }
                },
                scales: {
                    x: {
                        display: true,
                        title: {
                            display: true,
                            text: 'Дата'
                        }
                    },
                    y: {
                        display: true,
                        title: {
                            display: true,
                            text: 'Значение'
                        }
                    }
                }
            }
        });
        
        // График топ товаров
        const productsCtx = document.getElementById('topProductsChart').getContext('2d');
        const productsData = await this.loadChartData('top-products');
        
        this.charts.topProducts = new Chart(productsCtx, {
            type: 'doughnut',
            data: productsData,
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'bottom'
                    }
                }
            }
        });
        
        // График источников трафика
        const trafficCtx = document.getElementById('trafficSourcesChart').getContext('2d');
        const trafficData = await this.loadChartData('traffic-sources');
        
        this.charts.trafficSources = new Chart(trafficCtx, {
            type: 'pie',
            data: trafficData,
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'bottom'
                    }
                }
            }
        });
        
        // Спарклайны для KPI
        this.initSparklines();
    }
    
    async initSparklines() {
        const kpiCards = document.querySelectorAll('.kpi-card');
        
        for (const card of kpiCards) {
            const metric = card.dataset.metric;
            const canvas = card.querySelector('canvas');
            const ctx = canvas.getContext('2d');
            
            const sparklineData = await this.loadSparklineData(metric);
            
            new Chart(ctx, {
                type: 'line',
                data: sparklineData,
                options: {
                    responsive: false,
                    elements: {
                        point: {
                            radius: 0
                        }
                    },
                    plugins: {
                        legend: {
                            display: false
                        }
                    },
                    scales: {
                        x: {
                            display: false
                        },
                        y: {
                            display: false
                        }
                    }
                }
            });
        }
    }
    
    connectWebSocket() {
        const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
        this.websocket = new WebSocket(`${protocol}//${location.host}/ws/analytics`);
        
        this.websocket.onmessage = (event) => {
            const data = JSON.parse(event.data);
            this.handleRealtimeUpdate(data);
        };
        
        this.websocket.onclose = () => {
            // Переподключение через 5 секунд
            setTimeout(() => this.connectWebSocket(), 5000);
        };
    }
    
    handleRealtimeUpdate(data) {
        switch(data.type) {
            case 'kpi_update':
                this.updateKPICard(data.metric, data.value);
                break;
            case 'new_order':
                this.addNewOrderToTable(data.order);
                this.updateChartData(data);
                break;
            case 'user_activity':
                this.addUserActivity(data.activity);
                break;
        }
    }
    
    updateKPICard(metric, data) {
        const card = document.querySelector(`[data-metric="${metric}"]`);
        if (card) {
            const valueElement = card.querySelector('.kpi-value');
            const changeElement = card.querySelector('.kpi-change');
            
            // Анимированное обновление значения
            this.animateValue(valueElement, data.formatted_value);
            
            if (changeElement && data.change !== undefined) {
                changeElement.className = `kpi-change ${data.change > 0 ? 'positive' : (data.change < 0 ? 'negative' : 'neutral')}`;
                changeElement.innerHTML = `
                    <i class="fas fa-${data.change > 0 ? 'arrow-up' : (data.change < 0 ? 'arrow-down' : 'minus')}"></i>
                    ${Math.abs(data.change).toFixed(1)}%
                    <span class="change-period">vs ${data.compare_period_name}</span>
                `;
            }
        }
    }
    
    animateValue(element, newValue) {
        element.classList.add('updating');
        setTimeout(() => {
            element.innerHTML = newValue;
            element.classList.remove('updating');
        }, 200);
    }
    
    async loadChartData(chartType) {
        const response = await fetch(`/api/analytics/chart-data/${chartType}?${this.getFilterParams()}`);
        return response.json();
    }
    
    async loadSparklineData(metric) {
        const response = await fetch(`/api/analytics/sparkline/${metric}?${this.getFilterParams()}`);
        return response.json();
    }
    
    getFilterParams() {
        const period = document.getElementById('periodFilter').value;
        const compare = document.getElementById('compareFilter').value;
        const dateFrom = document.getElementById('dateFrom').value;
        const dateTo = document.getElementById('dateTo').value;
        
        const params = new URLSearchParams({
            period: period,
            compare: compare
        });
        
        if (period === 'custom' && dateFrom && dateTo) {
            params.append('date_from', dateFrom);
            params.append('date_to', dateTo);
        }
        
        return params.toString();
    }
    
    async updateDashboard() {
        // Показываем индикатор загрузки
        this.showLoading();
        
        try {
            // Обновляем KPI
            const kpiResponse = await fetch(`/api/analytics/kpi?${this.getFilterParams()}`);
            const kpiData = await kpiResponse.json();
            this.updateKPICards(kpiData);
            
            // Обновляем графики
            await this.updateAllCharts();
            
        } catch (error) {
            console.error('Failed to update dashboard:', error);
            this.showError('Ошибка обновления данных');
        } finally {
            this.hideLoading();
        }
    }
    
    startPeriodicUpdates() {
        // Обновляем данные каждые 30 секунд
        this.updateInterval = setInterval(() => {
            this.updateDashboard();
        }, 30000);
    }
}

// Инициализация dashboard
const dashboard = new AnalyticsDashboard();

// Глобальные функции
function updateDashboard() {
    dashboard.updateDashboard();
}

function exportReport() {
    const params = dashboard.getFilterParams();
    window.open(`/api/analytics/export?${params}`, '_blank');
}

function drillDown(metric) {
    window.open(`/analytics/drilldown/${metric}`, '_blank');
}
</script>
{% endblock %}

{% block page_scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="{{ asset('js/dashboard.js') }}"></script>
{% endblock %}
Поздравляем! Вы изучили продвинутые возможности MNRFY Framework. Эти примеры покрывают большинство сложных задач современной веб-разработки: от многоуровневых меню с правами доступа до систем аналитики в реальном времени.
Совет: Используйте эти примеры как основу для создания собственных компонентов. Комбинируйте различные техники для решения уникальных задач вашего проекта.