/** * TASKMATE - Dashboard Module * =========================== * Statistics and overview dashboard */ import store from './store.js'; import api from './api.js'; import { $, $$, createElement, clearElement, formatDate, getDueDateStatus, getInitials } from './utils.js'; class DashboardManager { constructor() { this.container = null; this.stats = null; this.completionData = null; this.timeData = null; this.init(); } init() { this.container = $('#view-dashboard'); // Subscribe to store changes store.subscribe('currentView', (view) => { if (view === 'dashboard') this.loadAndRender(); }); store.subscribe('currentProjectId', () => { if (store.get('currentView') === 'dashboard') { this.loadAndRender(); } }); } // ===================== // DATA LOADING // ===================== async loadAndRender() { if (store.get('currentView') !== 'dashboard') return; store.setLoading(true); try { const projectId = store.get('currentProjectId'); const [stats, completionData, timeData] = await Promise.all([ api.getStats(projectId), api.getCompletionStats(projectId, 8), api.getTimeStats(projectId) ]); this.stats = stats; this.completionData = completionData; this.timeData = timeData; // Due today tasks come from dashboard stats this.dueTodayTasks = stats.dueToday || []; // Overdue list - we only have the count, not individual tasks this.overdueTasks = []; this.render(); } catch (error) { console.error('Failed to load dashboard data:', error); this.showError('Fehler beim Laden der Dashboard-Daten'); } finally { store.setLoading(false); } } // ===================== // RENDERING // ===================== render() { this.renderStats(); this.renderCompletionChart(); this.renderTimeChart(); this.renderDueTodayList(); this.renderOverdueList(); } renderStats() { if (!this.stats) return; // Open tasks this.updateStatCard('stat-open', this.stats.open || 0); // In progress this.updateStatCard('stat-progress', this.stats.inProgress || 0); // Completed this.updateStatCard('stat-done', this.stats.completed || 0); // Overdue this.updateStatCard('stat-overdue', this.stats.overdue || 0); } updateStatCard(id, value) { const valueEl = $(`#${id}`); if (valueEl) { valueEl.textContent = value.toString(); } } renderCompletionChart() { const container = $('#chart-completed'); if (!container || !this.completionData) return; clearElement(container); // Add bar-chart class container.classList.add('bar-chart'); const maxValue = Math.max(...this.completionData.map(d => d.count), 1); this.completionData.forEach(item => { const percentage = (item.count / maxValue) * 100; const barItem = createElement('div', { className: 'bar-item' }, [ createElement('span', { className: 'bar-value' }, [item.count.toString()]), createElement('div', { className: 'bar', style: { height: `${Math.max(percentage, 5)}%` } }), createElement('span', { className: 'bar-label' }, [item.label || item.week]) ]); container.appendChild(barItem); }); } renderTimeChart() { const container = $('#chart-time'); if (!container || !this.timeData) return; clearElement(container); // Add horizontal-bar-chart class container.classList.add('horizontal-bar-chart'); const totalTime = this.timeData.reduce((sum, item) => sum + (item.totalMinutes || 0), 0); this.timeData.slice(0, 5).forEach(item => { const percentage = totalTime > 0 ? ((item.totalMinutes || 0) / totalTime) * 100 : 0; const barItem = createElement('div', { className: 'horizontal-bar-item' }, [ createElement('div', { className: 'horizontal-bar-header' }, [ createElement('span', { className: 'horizontal-bar-label' }, [item.name || item.projectName]), createElement('span', { className: 'horizontal-bar-value' }, [ this.formatMinutes(item.totalMinutes || 0) ]) ]), createElement('div', { className: 'horizontal-bar' }, [ createElement('div', { className: 'horizontal-bar-fill', style: { width: `${percentage}%` } }) ]) ]); container.appendChild(barItem); }); if (this.timeData.length === 0) { container.appendChild(createElement('p', { className: 'text-secondary', style: { textAlign: 'center' } }, ['Keine Zeitdaten verfügbar'])); } } renderDueTodayList() { const container = $('#due-today-list'); if (!container) return; clearElement(container); if (!this.dueTodayTasks || this.dueTodayTasks.length === 0) { container.appendChild(createElement('p', { className: 'text-secondary empty-message' }, ['Keine Aufgaben für heute'])); return; } this.dueTodayTasks.slice(0, 5).forEach(task => { const hasAssignee = task.assignedTo || task.assignedName; const item = createElement('div', { className: 'due-today-item', onclick: () => this.openTaskModal(task.id) }, [ createElement('span', { className: 'due-today-priority', style: { backgroundColor: this.getPriorityColor(task.priority) } }), createElement('span', { className: 'due-today-title' }, [task.title]), hasAssignee ? createElement('div', { className: 'due-today-assignee' }, [ createElement('span', { className: 'avatar avatar-sm', style: { backgroundColor: task.assignedColor || '#888' } }, [getInitials(task.assignedName || 'U')]) ]) : null ].filter(Boolean)); container.appendChild(item); }); if (this.dueTodayTasks.length > 5) { container.appendChild(createElement('button', { className: 'btn btn-ghost btn-sm btn-block', onclick: () => this.showAllDueToday() }, [`Alle ${this.dueTodayTasks.length} anzeigen`])); } } renderOverdueList() { const container = $('#overdue-list'); if (!container) return; clearElement(container); if (!this.overdueTasks || this.overdueTasks.length === 0) { container.appendChild(createElement('p', { className: 'text-secondary empty-message' }, ['Keine überfälligen Aufgaben'])); return; } this.overdueTasks.slice(0, 5).forEach(task => { const daysOverdue = this.getDaysOverdue(task.dueDate); const item = createElement('div', { className: 'due-today-item overdue-item', onclick: () => this.openTaskModal(task.id) }, [ createElement('span', { className: 'due-today-priority', style: { backgroundColor: this.getPriorityColor(task.priority) } }), createElement('div', { style: { flex: 1 } }, [ createElement('span', { className: 'due-today-title' }, [task.title]), createElement('span', { className: 'text-error', style: { fontSize: 'var(--text-xs)', display: 'block' } }, [`${daysOverdue} Tag(e) überfällig`]) ]), task.assignee ? createElement('div', { className: 'due-today-assignee' }, [ createElement('span', { className: 'avatar avatar-sm', style: { backgroundColor: task.assignee.color || '#888' } }, [getInitials(task.assignee.username)]) ]) : null ].filter(Boolean)); container.appendChild(item); }); if (this.overdueTasks.length > 5) { container.appendChild(createElement('button', { className: 'btn btn-ghost btn-sm btn-block', onclick: () => this.showAllOverdue() }, [`Alle ${this.overdueTasks.length} anzeigen`])); } } // ===================== // ACTIONS // ===================== openTaskModal(taskId) { window.dispatchEvent(new CustomEvent('modal:open', { detail: { modalId: 'task-modal', mode: 'edit', data: { taskId } } })); } showAllDueToday() { // Switch to list view with due date filter store.setFilter('dueDate', 'today'); store.setCurrentView('list'); } showAllOverdue() { // Switch to list view with overdue filter store.setFilter('dueDate', 'overdue'); store.setCurrentView('list'); } // ===================== // HELPERS // ===================== formatMinutes(minutes) { const hours = Math.floor(minutes / 60); const mins = minutes % 60; if (hours > 0) { return `${hours}h ${mins}m`; } return `${mins}m`; } getPriorityColor(priority) { const colors = { high: 'var(--priority-high)', medium: 'var(--priority-medium)', low: 'var(--priority-low)' }; return colors[priority] || colors.medium; } getDaysOverdue(dueDate) { const due = new Date(dueDate); const today = new Date(); const diffTime = today - due; return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } showError(message) { window.dispatchEvent(new CustomEvent('toast:show', { detail: { message, type: 'error' } })); } } // Create and export singleton const dashboardManager = new DashboardManager(); export default dashboardManager;