Initial commit
Dieser Commit ist enthalten in:
335
frontend/js/dashboard.js
Normale Datei
335
frontend/js/dashboard.js
Normale Datei
@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren