/
varsl
/
HTML-CSS
Обзор
Документация
Войти
/
varsl
/
HTML-CSS
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
frontend/js/dashboard.js
782 строки
28 KB
varsl
Добавление JavaScript логики
21 дек 2025, 23:16
21 дек 2025, 23:16
9dd2130
Код
Авторство
О чём код?
class Dashboard { constructor() { this.apiBaseUrl = 'http://localhost:3000/api'; this.charts = {}; this.userTickets = []; this.authManager = window.authManager; this.init(); } async init() { if (!this.authManager || !this.authManager.isAuthenticated()) { window.location.href = 'login.html'; return; } await this.loadUserData(); await this.loadDashboardData(); this.initCharts(); this.setupEventListeners(); this.setupNavigation(); this.loadUserTickets(); } async loadUserData() { const user = this.authManager.getUser(); if (user) { this.updateUserInfo(user); } else { // Пробуем загрузить данные const user = await this.authManager.getCurrentUser(); if (user) { this.updateUserInfo(user); } } } updateUserInfo(user) { const nameEl = document.querySelector('.user-name'); const emailEl = document.querySelector('.user-email'); const phoneEl = document.querySelector('.user-phone'); const welcomeMessage = document.querySelector('.dashboard-subtitle'); const daysWithUs = document.querySelector('.stat-days'); if (nameEl) { nameEl.textContent = user.name || 'Пользователь'; } if (emailEl) { emailEl.textContent = user.email || ''; } if (phoneEl) { phoneEl.textContent = user.phone || 'Телефон не указан'; } if (welcomeMessage) { welcomeMessage.textContent = `Добро пожаловать, ${user.name || 'Пользователь'}!`; } // Расчет дней с момента регистрации if (daysWithUs && user.created_at) { const createdDate = new Date(user.created_at); const today = new Date(); const diffTime = Math.abs(today - createdDate); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); daysWithUs.textContent = diffDays; } this.updateAvatar(user); } updateAvatar(user) { const avatarEl = document.querySelector('.user-avatar'); if (!avatarEl) return; avatarEl.innerHTML = ''; if (user.avatar) { const img = document.createElement('img'); img.src = user.avatar; img.alt = user.name || 'Аватар'; img.style.width = '100%'; img.style.height = '100%'; img.style.borderRadius = '50%'; img.style.objectFit = 'cover'; avatarEl.appendChild(img); } else { const initials = user.name ? user.name.charAt(0).toUpperCase() : 'П'; avatarEl.textContent = initials; avatarEl.style.display = 'flex'; avatarEl.style.alignItems = 'center'; avatarEl.style.justifyContent = 'center'; avatarEl.style.fontSize = '24px'; avatarEl.style.fontWeight = 'bold'; avatarEl.style.color = '#fff'; avatarEl.style.backgroundColor = '#4CAF50'; } } async loadDashboardData() { try { const response = await fetch(`${this.apiBaseUrl}/auth/dashboard-data`, { headers: { 'Authorization': `Bearer ${this.authManager.getToken()}` } }); if (!response.ok) { if (response.status === 401) { this.authManager.logout(); return; } throw new Error(`Ошибка загрузки данных: ${response.status}`); } const data = await response.json(); if (data.success) { this.updateStats(data.stats); this.updateChartsData(data.charts); if (data.recentActivity && data.recentActivity.length > 0) { this.renderActivity(data.recentActivity); } else { this.showDefaultActivity(); } if (data.achievements && data.achievements.length > 0) { this.showAchievementsSection(); this.renderAchievements(data.achievements); } else { this.hideAchievementsSection(); } } else { throw new Error(data.message || 'Ошибка загрузки данных'); } } catch (error) { console.error('Ошибка загрузки дашборда:', error); this.showDefaultActivity(); this.hideAchievementsSection(); } } updateStats(stats) { if (!stats) return; const statElements = { tickets: document.querySelector('.stat-tickets'), visits: document.querySelector('.stat-visits'), spending: document.querySelector('.stat-spending'), days: document.querySelector('.stat-days') }; if (statElements.tickets) { statElements.tickets.textContent = stats.tickets || 0; } if (statElements.visits) { statElements.visits.textContent = stats.visits || 0; } if (statElements.spending) { const spending = stats.spending || 0; statElements.spending.textContent = `${spending.toLocaleString('ru-RU')} ₽`; } } async loadUserTickets() { try { const response = await fetch(`${this.apiBaseUrl}/tickets/my`, { headers: { 'Authorization': `Bearer ${this.authManager.getToken()}` } }); if (!response.ok) { throw new Error('Ошибка загрузки билетов'); } const data = await response.json(); if (data.success && data.tickets) { this.userTickets = data.tickets; this.renderTickets(this.userTickets); this.updateTotalSpending(); } else { this.showNoTickets(); } } catch (error) { console.error('Ошибка загрузки билетов:', error); this.showNoTickets(); } finally { this.hideTicketsLoading(); } } renderTickets(tickets) { const ticketsBody = document.getElementById('ticketsBody'); const ticketsTable = document.getElementById('ticketsTable'); const noTickets = document.getElementById('noTickets'); if (!ticketsBody) return; ticketsBody.innerHTML = ''; if (!tickets || tickets.length === 0) { this.showNoTickets(); return; } if (ticketsTable) ticketsTable.style.display = 'table'; if (noTickets) noTickets.style.display = 'none'; tickets.forEach(ticket => { const row = document.createElement('tr'); row.setAttribute('data-category', ticket.status || 'active'); const formattedDate = new Date(ticket.visit_date).toLocaleDateString('ru-RU'); const formattedPrice = parseFloat(ticket.total_amount || 0).toLocaleString('ru-RU'); let statusClass = 'active'; let statusText = 'Активный'; switch(ticket.status) { case 'used': statusClass = 'used'; statusText = 'Использован'; break; case 'cancelled': statusClass = 'cancelled'; statusText = 'Отменен'; break; } row.innerHTML = ` <td data-id="${ticket.order_id || 'N/A'}">${ticket.order_id || 'N/A'}</td> <td data-date="${ticket.visit_date}">${formattedDate}</td> <td data-type="${ticket.ticket_type || 'Стандартный'}">${ticket.customer_name || 'Посетитель'}</td> <td data-price="${ticket.total_amount || 0}">${formattedPrice} ₽</td> <td><span class="status-badge ${statusClass}">${statusText}</span></td> <td> <div class="table-actions"> <button class="btn-icon view-ticket" title="Просмотр" data-id="${ticket.id}"> <i class="fas fa-eye"></i> </button> <button class="btn-icon download-ticket" title="Скачать" data-id="${ticket.id}"> <i class="fas fa-download"></i> </button> </div> </td> `; ticketsBody.appendChild(row); }); this.setupTicketActions(); } setupTicketActions() { document.querySelectorAll('.view-ticket').forEach(btn => { btn.addEventListener('click', (e) => { const ticketId = e.target.closest('button').getAttribute('data-id'); this.viewTicket(ticketId); }); }); document.querySelectorAll('.download-ticket').forEach(btn => { btn.addEventListener('click', (e) => { const ticketId = e.target.closest('button').getAttribute('data-id'); this.downloadTicket(ticketId); }); }); } viewTicket(ticketId) { const ticket = this.userTickets.find(t => t.id == ticketId); if (ticket) { alert(`Билет №${ticket.order_id}\nДата посещения: ${new Date(ticket.visit_date).toLocaleDateString('ru-RU')}\nСумма: ${ticket.total_amount} ₽\nСтатус: ${ticket.status === 'active' ? 'Активный' : ticket.status === 'used' ? 'Использован' : 'Отменен'}`); } } downloadTicket(ticketId) { const ticket = this.userTickets.find(t => t.id == ticketId); if (ticket) { // В реальном приложении здесь был бы запрос на сервер для генерации PDF alert(`Билет №${ticket.order_id} будет скачан`); } } updateTotalSpending() { if (!this.userTickets || this.userTickets.length === 0) return; const totalSpending = this.userTickets.reduce((sum, ticket) => { return sum + parseFloat(ticket.total_amount || 0); }, 0); const spendingElement = document.querySelector('.stat-spending'); if (spendingElement) { spendingElement.textContent = `${totalSpending.toLocaleString('ru-RU')} ₽`; } } showNoTickets() { const ticketsTable = document.getElementById('ticketsTable'); const noTickets = document.getElementById('noTickets'); const ticketsLoading = document.getElementById('ticketsLoading'); if (ticketsTable) ticketsTable.style.display = 'none'; if (noTickets) noTickets.style.display = 'block'; if (ticketsLoading) ticketsLoading.style.display = 'none'; } hideTicketsLoading() { const ticketsLoading = document.getElementById('ticketsLoading'); if (ticketsLoading) { ticketsLoading.style.display = 'none'; } } initCharts() { this.initVisitsChart(); } initVisitsChart() { const ctx = document.getElementById('visitsChart'); if (!ctx) return; this.charts.visits = new Chart(ctx.getContext('2d'), { type: 'line', data: { labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'], datasets: [{ label: 'Посещения', data: Array(12).fill(0), borderColor: '#2E7D32', backgroundColor: 'rgba(46, 125, 50, 0.1)', borderWidth: 2, fill: true, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { return `Посещений: ${context.parsed.y}`; } } } }, scales: { y: { beginAtZero: true, grid: { drawBorder: false }, ticks: { precision: 0 } }, x: { grid: { display: false } } } } }); } updateChartsData(chartData) { if (!chartData) return; if (chartData.visits && this.charts.visits) { this.charts.visits.data.datasets[0].data = chartData.visits; this.charts.visits.update('none'); } } showDefaultActivity() { const container = document.getElementById('activityList'); if (!container) return; container.innerHTML = ` <div class="activity-item"> <div class="activity-icon"> <i class="fas fa-info-circle"></i> </div> <div class="activity-content"> <div class="activity-title">Добро пожаловать в зоопарк!</div> <div class="activity-time">Начните с покупки билета</div> </div> </div> `; } renderActivity(activities) { const container = document.getElementById('activityList'); if (!container) return; if (!activities || activities.length === 0) { this.showDefaultActivity(); return; } container.innerHTML = ''; activities.slice(0, 5).forEach(activity => { const activityEl = document.createElement('div'); activityEl.className = 'activity-item'; const icon = this.getActivityIcon(activity.type); const time = this.formatTime(activity.timestamp); activityEl.innerHTML = ` <div class="activity-icon"> <i class="fas fa-${icon}"></i> </div> <div class="activity-content"> <div class="activity-title">${activity.title}</div> <div class="activity-time">${time}</div> </div> `; container.appendChild(activityEl); }); } showAchievementsSection() { const achievementsSection = document.getElementById('achievementsSection'); if (achievementsSection) { achievementsSection.style.display = 'block'; } } hideAchievementsSection() { const achievementsSection = document.getElementById('achievementsSection'); if (achievementsSection) { achievementsSection.style.display = 'none'; } } renderAchievements(achievements) { const container = document.getElementById('achievementsList'); if (!container) return; container.innerHTML = ''; achievements.forEach(achievement => { const achievementEl = document.createElement('div'); achievementEl.className = 'achievement-card'; achievementEl.innerHTML = ` <div class="achievement-icon"> <i class="fas fa-trophy"></i> </div> <div class="achievement-content"> <h4>${achievement.title}</h4> <p>${achievement.description}</p> </div> `; container.appendChild(achievementEl); }); } getActivityIcon(type) { const icons = { ticket: 'ticket-alt', visit: 'calendar-check', review: 'comment', favorite: 'heart', profile: 'user-edit', payment: 'credit-card' }; return icons[type] || 'bell'; } formatTime(timestamp) { try { const date = new Date(timestamp); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) { return 'Только что'; } else if (diffMins < 60) { return `${diffMins} ${this.getRussianWord(diffMins, ['минуту', 'минуты', 'минут'])} назад`; } else if (diffHours < 24) { return `${diffHours} ${this.getRussianWord(diffHours, ['час', 'часа', 'часов'])} назад`; } else if (diffDays < 7) { return `${diffDays} ${this.getRussianWord(diffDays, ['день', 'дня', 'дней'])} назад`; } else { return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }); } } catch (error) { return 'Недавно'; } } getRussianWord(number, words) { const cases = [2, 0, 1, 1, 1, 2]; return words[(number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5]]; } setupEventListeners() { this.setupTableSorting(); this.setupFilters(); this.setupRefreshButton(); this.setupChartFilters(); } setupNavigation() { // Кнопки быстрых действий const buyTicketBtn = document.getElementById('buyTicketBtn'); if (buyTicketBtn) { buyTicketBtn.addEventListener('click', () => { window.location.href = 'ticket.html'; }); } const bookExcursionBtn = document.getElementById('bookExcursionBtn'); if (bookExcursionBtn) { bookExcursionBtn.addEventListener('click', () => { window.location.href = 'excursion.html'; }); } const viewAnimalsBtn = document.getElementById('viewAnimalsBtn'); if (viewAnimalsBtn) { viewAnimalsBtn.addEventListener('click', () => { window.location.href = 'animals.html'; }); } const editProfileBtn = document.getElementById('editProfileBtn'); if (editProfileBtn) { editProfileBtn.addEventListener('click', () => { window.location.href = 'profile.html'; }); } const historyBtn = document.getElementById('historyBtn'); if (historyBtn) { historyBtn.addEventListener('click', (e) => { e.preventDefault(); alert('История посещений в разработке'); }); } } setupRefreshButton() { const refreshBtn = document.getElementById('refreshData'); if (refreshBtn) { refreshBtn.addEventListener('click', async () => { refreshBtn.classList.add('rotate-animation'); await this.loadDashboardData(); await this.loadUserTickets(); setTimeout(() => { refreshBtn.classList.remove('rotate-animation'); }, 1000); }); } } setupChartFilters() { document.querySelectorAll('.chart-filter').forEach(filter => { filter.addEventListener('click', function() { const chartType = this.getAttribute('data-chart'); const filterType = this.getAttribute('data-filter'); this.parentElement.querySelectorAll('.chart-filter').forEach(btn => { btn.classList.remove('active'); }); this.classList.add('active'); // Здесь можно добавить обновление графика в зависимости от фильтра console.log(`Фильтр графика: ${chartType}, тип: ${filterType}`); }); }); } setupTableSorting() { const tableHeaders = document.querySelectorAll('.data-table th[data-sort]'); tableHeaders.forEach(header => { header.addEventListener('click', () => { const sortBy = header.getAttribute('data-sort'); const currentOrder = header.getAttribute('data-order') || 'asc'; const newOrder = currentOrder === 'asc' ? 'desc' : 'asc'; tableHeaders.forEach(h => { h.removeAttribute('data-order'); const icon = h.querySelector('i'); if (icon) { icon.className = 'fas fa-sort'; } }); header.setAttribute('data-order', newOrder); const icon = header.querySelector('i'); if (icon) { icon.className = `fas fa-sort-${newOrder === 'asc' ? 'up' : 'down'}`; } this.sortTable(sortBy, newOrder); }); }); } sortTable(sortBy, order) { const table = document.querySelector('.data-table tbody'); if (!table) return; const rows = Array.from(table.querySelectorAll('tr')); rows.sort((a, b) => { const aCell = a.querySelector(`td[data-${sortBy}]`); const bCell = b.querySelector(`td[data-${sortBy}]`); if (!aCell || !bCell) return 0; const aValue = aCell.getAttribute(`data-${sortBy}`); const bValue = bCell.getAttribute(`data-${sortBy}`); if (sortBy === 'date') { return order === 'asc' ? new Date(aValue) - new Date(bValue) : new Date(bValue) - new Date(aValue); } else if (sortBy === 'price') { return order === 'asc' ? parseFloat(aValue) - parseFloat(bValue) : parseFloat(bValue) - parseFloat(aValue); } else { return order === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); } }); rows.forEach(row => table.appendChild(row)); } setupFilters() { const searchInput = document.getElementById('searchInput'); const filterSelect = document.getElementById('filterSelect'); if (searchInput) { searchInput.addEventListener('input', (e) => { this.filterTable(e.target.value); }); } if (filterSelect) { filterSelect.addEventListener('change', (e) => { this.filterTableByCategory(e.target.value); }); } } filterTable(searchTerm) { const rows = document.querySelectorAll('.data-table tbody tr'); const term = searchTerm.toLowerCase(); rows.forEach(row => { const text = row.textContent.toLowerCase(); row.style.display = text.includes(term) ? '' : 'none'; }); } filterTableByCategory(category) { const rows = document.querySelectorAll('.data-table tbody tr'); rows.forEach(row => { const rowCategory = row.getAttribute('data-category'); if (category === 'all' || rowCategory === category) { row.style.display = ''; } else { row.style.display = 'none'; } }); } showError(message) { const errorEl = document.createElement('div'); errorEl.className = 'alert alert-error'; errorEl.innerHTML = ` <i class="fas fa-exclamation-circle"></i> <span>${message}</span> <button class="alert-close"> <i class="fas fa-times"></i> </button> `; const container = document.querySelector('.dashboard-content'); if (container) { const header = container.querySelector('.dashboard-header'); if (header) { container.insertBefore(errorEl, header.nextSibling); } setTimeout(() => { errorEl.classList.add('show'); }, 10); const closeBtn = errorEl.querySelector('.alert-close'); closeBtn.addEventListener('click', () => { errorEl.classList.remove('show'); setTimeout(() => errorEl.remove(), 300); }); setTimeout(() => { errorEl.classList.remove('show'); setTimeout(() => errorEl.remove(), 300); }, 5000); } } showSuccess(message) { const successEl = document.createElement('div'); successEl.className = 'alert alert-success'; successEl.innerHTML = ` <i class="fas fa-check-circle"></i> <span>${message}</span> <button class="alert-close"> <i class="fas fa-times"></i> </button> `; const container = document.querySelector('.dashboard-content'); if (container) { const header = container.querySelector('.dashboard-header'); if (header) { container.insertBefore(successEl, header.nextSibling); } setTimeout(() => { successEl.classList.add('show'); }, 10); const closeBtn = successEl.querySelector('.alert-close'); closeBtn.addEventListener('click', () => { successEl.classList.remove('show'); setTimeout(() => successEl.remove(), 300); }); setTimeout(() => { successEl.classList.remove('show'); setTimeout(() => successEl.remove(), 300); }, 3000); } } } // Инициализация при загрузке страницы document.addEventListener('DOMContentLoaded', function() { // Инициализируем Chart.js глобально if (typeof Chart !== 'undefined') { Chart.defaults.font.family = "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"; Chart.defaults.color = '#666'; Chart.defaults.plugins.legend.labels.font = { family: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif", size: 12 }; } // Запускаем дашборд const dashboard = new Dashboard(); // Делаем dashboard глобально доступным для отладки window.dashboard = dashboard; });