Hydrant129's picture
Initial DeepSite commit
13ab0ed verified
/**
* TaskList View Component
* Vollständige Task-Verwaltung mit Filtern, Sortierung und Drag & Drop
*/
import { store } from '../state.js';
import { Task } from '../models.js';
import { Route } from '../router.js';
export class TaskList {
constructor(projectId = null) {
this.projectId = projectId;
this.viewMode = 'list'; // 'list' | 'board' | 'calendar'
this.draggedTask = null;
}
render(container) {
this.container = container;
this.update();
// Subscriptions
this.unsubscribers = [
store.subscribe('tasks:changed', () => this.update()),
store.subscribe('projects:changed', () => this.update()),
store.subscribe('ui:searchQuery:changed', () => this.update())
];
}
update() {
let tasks = this.projectId
? store.getTasksByProject(this.projectId)
: store.getFilteredTasks();
// Filter nach aktuellem User wenn "Meine Tasks"
if (!this.projectId && window.location.pathname === Route.TASKS) {
tasks = tasks.filter(t => t.assigneeId === store.state.currentUser?.id);
}
this.container.innerHTML = `
<div class="animate-fade-in h-full flex flex-col">
<!-- Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
${this.projectId ? this.getProjectName() : 'Meine Tasks'}
</h1>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
${tasks.length} Tasks insgesamt
</p>
</div>
<div class="flex items-center space-x-3">
<!-- View Toggle -->
<div class="bg-gray-100 dark:bg-slate-700 p-1 rounded-lg flex">
${['list', 'board'].map(mode => `
<button onclick="window.setViewMode('${mode}')"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${this.viewMode === mode ? 'bg-white dark:bg-slate-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white'}">
<i data-lucide="${mode === 'list' ? 'list' : 'layout-grid'}" class="w-4 h-4"></i>
</button>
`).join('')}
</div>
<button onclick="window.openTaskModal()" class="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
Neuer Task
</button>
</div>
</div>
<!-- Filters -->
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-sm border border-gray-200 dark:border-slate-700 p-4 mb-6">
<div class="flex flex-wrap gap-4 items-center">
<div class="flex-1 min-w-[200px]">
<div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"></i>
<input type="text"
value="${store.state.ui.searchQuery}"
oninput="window.updateSearch(this.value)"
placeholder="Tasks suchen..."
class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:text-white">
</div>
</div>
<select onchange="window.updateFilter('priority', this.value)" class="border border-gray-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-sm dark:text-white">
<option value="">Alle Prioritäten</option>
${Object.values(Task.PRIORITY).map(p => `
<option value="${p}" ${store.state.ui.filters.priority === p ? 'selected' : ''}>${this.capitalize(p)}</option>
`).join('')}
</select>
<select onchange="window.updateFilter('status', this.value)" class="border border-gray-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-sm dark:text-white">
<option value="">Alle Status</option>
${Object.values(Task.STATUS).map(s => `
<option value="${s}" ${store.state.ui.filters.status === s ? 'selected' : ''}>${this.formatStatus(s)}</option>
`).join('')}
</select>
${this.projectId ? '' : `
<select onchange="window.updateFilter('project', this.value)" class="border border-gray-300 dark:border-slate-600 rounded-lg px-3 py-2 bg-white dark:bg-slate-700 text-sm dark:text-white">
<option value="">Alle Projekte</option>
${store.state.projects.map(p => `
<option value="${p.id}" ${store.state.ui.filters.project === p.id ? 'selected' : ''}>${p.name}</option>
`).join('')}
</select>
`}
<button onclick="window.clearFilters()" class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-hidden">
${this.viewMode === 'board' ? this.renderBoardView(tasks) : this.renderListView(tasks)}
</div>
</div>
`;
lucide.createIcons();
this.bindEvents();
}
renderListView(tasks) {
if (tasks.length === 0) {
return this.renderEmptyState();
}
const sortedTasks = this.sortTasks(tasks);
return `
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-gray-200 dark:border-slate-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
<thead class="bg-gray-50 dark:bg-slate-700/50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-8"></th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Task</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Priorität</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Projekt</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Fällig</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Assignee</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Aktionen</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
${sortedTasks.map(task => this.renderTaskRow(task)).join('')}
</tbody>
</table>
</div>
</div>
`;
}
renderBoardView(tasks) {
const columns = [
{ id: 'todo', title: 'To Do', color: 'gray' },
{ id: 'in_progress', title: 'In Progress', color: 'blue' },
{ id: 'review', title: 'Review', color: 'yellow' },
{ id: 'done', title: 'Done', color: 'green' }
];
return `
<div class="h-full overflow-x-auto custom-scrollbar">
<div class="flex space-x-6 min-w-full h-full">
${columns.map(col => {
const colTasks = tasks.filter(t => t.status === col.id);
return `
<div class="flex-1 min-w-[300px] max-w-[400px] flex flex-col">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-3 h-3 rounded-full bg-${col.color}-500 mr-2"></div>
<h3 class="font-semibold text-gray-900 dark:text-white">${col.title}</h3>
<span class="ml-2 bg-gray-100 dark:bg-slate-700 text-gray-600 dark:text-gray-300 text-xs px-2 py-0.5 rounded-full">${colTasks.length}</span>
</div>
</div>
<div class="flex-1 space-y-3 overflow-y-auto custom-scrollbar pr-2"
ondrop="window.handleDrop(event, '${col.id}')"
ondragover="window.handleDragOver(event)"
data-status="${col.id}">
${colTasks.map(task => this.renderTaskCard(task)).join('')}
${colTasks.length === 0 ? `
<div class="border-2 border-dashed border-gray-300 dark:border-slate-600 rounded-lg p-8 text-center text-gray-400 dark:text-gray-500 text-sm">
Drop tasks here
</div>
` : ''}
</div>
</div>
`;
}).join('')}
</div>
</div>
`;
}
renderTaskRow(task) {
const project = store.state.projects.find(p => p.id === task.projectId);
const assignee = store.state.users.find(u => u.id === task.assigneeId);
const priorityColors = {
low: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
medium: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
urgent: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
};
const statusColors = {
todo: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
in_progress: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
review: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
done: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
};
return `
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap">
<button onclick="window.toggleTaskStatus('${task.id}')"
class="w-5 h-5 rounded border-2 ${task.status === 'done' ? 'bg-green-500 border-green-500' : 'border-gray-300 dark:border-slate-500 hover:border-primary-500'} flex items-center justify-center transition-colors">
${task.status === 'done' ? '<i data-lucide="check" class="w-3 h-3 text-white"></i>' : ''}
</button>
</td>
<td class="px-6 py-4">
<div class="text-sm font-medium text-gray-900 dark:text-white ${task.status === 'done' ? 'line-through text-gray-400 dark:text-gray-500' : ''}">${task.title}</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate max-w-xs">${task.description || 'Keine Beschreibung'}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs rounded-full ${statusColors[task.status]}">${this.formatStatus(task.status)}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs rounded-full ${priorityColors[task.priority]}">${this.capitalize(task.priority)}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
${project ? `
<div class="flex items-center">
<span class="w-2 h-2 rounded-full mr-2" style="background-color: ${project.color}"></span>
<span class="text-sm text-gray-600 dark:text-gray-300">${project.name}</span>
</div>
` : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap">
${task.dueDate ? `
<div class="flex items-center text-sm ${task.isOverdue ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-gray-600 dark:text-gray-300'}">
<i data-lucide="calendar" class="w-3 h-3 mr-1"></i>
${new Date(task.dueDate).toLocaleDateString('de-DE')}
${task.isOverdue ? '<span class="ml-1 text-xs">(überfällig)</span>' : ''}
</div>
` : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap">
${assignee ? `
<img src="${assignee.avatar}" alt="${assignee.name}" class="w-8 h-8 rounded-full" title="${assignee.name}">
` : '-'}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<button onclick="window.editTask('${task.id}')" class="text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 mr-3">
<i data-lucide="pencil" class="w-4 h-4"></i>
</button>
<button onclick="window.deleteTask('${task.id}')" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</td>
</tr>
`;
}
renderTaskCard(task) {
const project = store.state.projects.find(p => p.id === task.projectId);
const assignee = store.state.users.find(u => u.id === task.assigneeId);
const priorityColors = {
low: 'border-l-4 border-gray-400',
medium: 'border-l-4 border-blue-500',
high: 'border-l-4 border-orange-500',
urgent: 'border-l-4 border-red-500'
};
return `
<div class="task-card bg-white dark:bg-slate-700 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-slate-600 ${priorityColors[task.priority]} cursor-pointer"
draggable="true"
ondragstart="window.handleDragStart(event, '${task.id}')"
onclick="window.editTask('${task.id}')">
<div class="flex justify-between items-start mb-2">
<h4 class="text-sm font-medium text-gray-900 dark:text-white line-clamp-2">${task.title}</h4>
${task.isOverdue ? '<span class="text-xs text-red-500 font-semibold">!</span>' : ''}
</div>
<div class="flex items-center justify-between mt-3">
<div class="flex items-center space-x-2">
${assignee ? `<img src="${assignee.avatar}" class="w-6 h-6 rounded-full" title="${assignee.name}">` : ''}
${task.dueDate ? `
<span class="text-xs ${task.isOverdue ? 'text-red-500' : 'text-gray-500 dark:text-gray-400'} flex items-center">
<i data-lucide="clock" class="w-3 h-3 mr-1"></i>
${new Date(task.dueDate).toLocaleDateString('de-DE', {month:'short', day:'numeric'})}
</span>
` : ''}
</div>
${project ? `<span class="w-2 h-2 rounded-full" style="background-color: ${project.color}" title="${project.name}"></span>` : ''}
</div>
</div>
`;
}
renderEmptyState() {
return `
<div class="text-center py-12 bg-white dark:bg-slate-800 rounded-xl border border-gray-200 dark:border-slate-700">
<i data-lucide="inbox" class="w-12 h-12 mx-auto text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Keine Tasks gefunden</h3>
<p class="text-gray-500 dark:text-gray-400 mb-4">Erstellen Sie einen neuen Task oder passen Sie die Filter an.</p>
<button onclick="window.openTaskModal()" class="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700">
<i data-lucide="plus" class="w-4 h-4 mr-2"></i>
Neuer Task
</button>
</div>
`;
}
getProjectName() {
const project = store.state.projects.find(p => p.id === this.projectId);
return project ? project.name : 'Unbekanntes Projekt';
}
sortTasks(tasks) {
return tasks.sort((a, b) => {
// Überfällige zuerst
if (a.isOverdue && !b.isOverdue) return -1;
if (!a.isOverdue && b.isOverdue) return 1;
// Dann nach Priorität
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 };
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
return priorityOrder[a.priority] - priorityOrder[b.priority];
}
// Dann nach Fälligkeitsdatum
if (a.dueDate && b.dueDate) {
return new Date(a.dueDate) - new Date(b.dueDate);
}
return 0;
});
}
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
formatStatus(status) {
const labels = {
todo: 'To Do',
in_progress: 'In Progress',
review: 'Review',
done: 'Done'
};
return labels[status] || status;
}
bindEvents() {
// View mode toggle
window.setViewMode = (mode) => {
this.viewMode = mode;
this.update();
};
// Filters
window.updateSearch = (value) => {
store.setUISState('searchQuery', value);
};
window.updateFilter = (type, value) => {
const filters = { ...store.state.ui.filters, [type]: value || null };
store.setUISState('filters', filters);
};
window.clearFilters = () => {
store.setUISState('searchQuery', '');
store.setUISState('filters', { priority: null, status: null, assignee: null, project: null });
};
// Task CRUD
window.openTaskModal = () => {
const title = prompt('Task Titel:');
if (!title) return;
const description = prompt('Beschreibung:') || '';
const priority = prompt('Priorität (low, medium, high, urgent):', 'medium') || 'medium';
store.addTask({
title,
description,
projectId: this.projectId || (store.state.projects[0]?.id || null),
assigneeId: store.state.currentUser?.id,
creatorId: store.state.currentUser?.id,
priority: ['low', 'medium', 'high', 'urgent'].includes(priority) ? priority : 'medium',
status: 'todo',
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // +7 days default
});
};
window.editTask = (taskId) => {
const task = store.state.tasks.find(t => t.id === taskId);
if (!task) return;
const title = prompt('Neuer Titel:', task.title);
if (title === null) return;
const status = prompt('Status (todo, in_progress, review, done):', task.status);
if (status === null) return;
const priority = prompt('Priorität (low, medium, high, urgent):', task.priority);
if (priority === null) return;
const updates = { title };
if (['todo', 'in_progress', 'review', 'done'].includes(status)) updates.status = status;
if (['low', 'medium', 'high', 'urgent'].includes(priority)) updates.priority = priority;
store.updateTask(taskId, updates);
};
window.deleteTask = (taskId) => {
if (confirm('Task wirklich löschen?')) {
store.deleteTask(taskId);
}
};
window.toggleTaskStatus = (taskId) => {
const task = store.state.tasks.find(t => t.id === taskId);
if (task) {
const newStatus = task.status === 'done' ? 'todo' : 'done';
store.updateTask(taskId, { status: newStatus });
}
};
// Drag & Drop
window.handleDragStart = (e, taskId) => {
this.draggedTask = taskId;
e.dataTransfer.effectAllowed = 'move';
e.target.style.opacity = '0.5';
};
window.handleDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
e.currentTarget.classList.add('bg-gray-100', 'dark:bg-slate-700/50');
};
window.handleDrop = (e, status) => {
e.preventDefault();
e.currentTarget.classList.remove('bg-gray-100', 'dark:bg-slate-700/50');
const taskId = this.draggedTask;
if (taskId) {
store.moveTask(taskId, status);
this.draggedTask = null;
}
// Reset opacity
document.querySelectorAll('[draggable="true"]').forEach(el => el.style.opacity = '1');
};
}
destroy() {
this.unsubscribers.forEach(unsub => unsub());
// Cleanup global functions
delete window.setViewMode;
delete window.updateSearch;
delete window.updateFilter;
delete window.clearFilters;
delete window.openTaskModal;
delete window.editTask;
delete window.deleteTask;
delete window.toggleTaskStatus;
delete window.handleDragStart;
delete window.handleDragOver;
delete window.handleDrop;
}
}