Files
TaskMate/frontend/js/dashboard.js
Claude Project Manager ab1e5be9a9 Initial commit
2025-12-28 21:36:45 +00:00

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;