336 Zeilen
9.3 KiB
JavaScript
336 Zeilen
9.3 KiB
JavaScript
/**
|
|
* 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;
|