Files
MindSift/frontend/src/features/triage/TasksView.tsx
T
sandrodach 177b69234c
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
feat: unify sharing modal and clean up team share duplication in lists
2026-10-25 17:41:18 +01:00

1626 lines
65 KiB
TypeScript

import React, { useEffect, useState, useRef, useMemo } from 'react';
import {
Check,
Plus,
Calendar,
Trash2,
RotateCcw,
FileText,
LayoutList,
Columns,
ArrowRight,
ArrowLeft,
Edit3,
X,
Layers,
Share,
Share2,
Users,
CheckSquare,
User as UserIcon,
Lock,
Hash,
AlertTriangle,
Flame,
Settings,
ChevronDown,
Folder,
} from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { TaskDetailModal } from './TaskDetailModal';
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal';
import { UserAutocomplete } from '../UserAutocomplete';
const getInitials = (name?: string): string => {
if (!name) return '?';
const parts = name.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return name.slice(0, 2).toUpperCase();
};
interface TasksViewProps {
currentUser?: User | null;
onRefreshStats: () => void;
onOpenDocument: (docId: string) => void;
refreshTrigger?: number;
}
export const TasksView: React.FC<TasksViewProps> = ({
currentUser,
onRefreshStats,
onOpenDocument,
refreshTrigger = 0,
}) => {
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
const [tasks, setTasks] = useState<Thought[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
const [selectedListId, setSelectedListId] = useState<string>('all');
const [teams, setTeams] = useState<Team[]>([]);
const [collaborators, setCollaborators] = useState<User[]>([]);
const [discarded, setDiscarded] = useState<Thought[]>([]);
const [loading, setLoading] = useState(true);
// Quick Task Add state
const [quickTitle, setQuickTitle] = useState('');
const [quickDueDate, setQuickDueDate] = useState('');
// New TaskList modal state
const [showNewListModal, setShowNewListModal] = useState(false);
const [newListTitle, setNewListTitle] = useState('');
const [newListIcon, setNewListIcon] = useState('📋');
const [newListShareTarget, setNewListShareTarget] = useState('');
// Share TaskList modal state
const [showShareListModal, setShowShareListModal] = useState(false);
const [sharingTaskList, setSharingTaskList] = useState<TaskList | null>(null);
const [addShareType, setAddShareType] = useState<'team' | 'user'>('team');
const [selectedShareTargetId, setSelectedShareTargetId] = useState('');
const [selectedShareUser, setSelectedShareUser] = useState<User | null>(null);
const [shareLoading, setShareLoading] = useState(false);
// Detail modal state
const [detailTaskId, setDetailTaskId] = useState<string | null>(null);
const [showDetailModal, setShowDetailModal] = useState(false);
// Manage columns modal state
const [showManageColumnsModal, setShowManageColumnsModal] = useState(false);
const [globalColumns, setGlobalColumns] = useState<KanbanColumn[]>(() => {
try {
const stored = localStorage.getItem('mindsift:kanban_columns');
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
}
} catch {}
return DEFAULT_KANBAN_COLUMNS;
});
// Hashtag filtering, grouping and sorting state
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const [groupBy, setGroupBy] = useState<'none' | 'tag' | 'status' | 'priority' | 'due'>('none');
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'alpha'>('created');
const loadData = async () => {
setLoading(true);
try {
const [allTasks, allLists, allTeams, allCollabs, allDiscarded] = await Promise.all([
api.getTasks(selectedListId === 'all' ? undefined : selectedListId).catch(() => []),
api.getTaskLists().catch(() => []),
api.getUserTeams().catch(() => []),
api.getCollaborators().catch(() => []),
api.getThoughts('discarded').catch(() => []),
]);
setTasks(Array.isArray(allTasks) ? allTasks : []);
setTaskLists(Array.isArray(allLists) ? allLists : []);
setTeams(Array.isArray(allTeams) ? allTeams : []);
setCollaborators(Array.isArray(allCollabs) ? allCollabs : []);
setDiscarded(Array.isArray(allDiscarded) ? allDiscarded : []);
} catch {
// Prevent unhandled promise rejection from crashing view
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [refreshTrigger, selectedListId]);
const selectedListIdRef = useRef(selectedListId);
selectedListIdRef.current = selectedListId;
const onRefreshStatsRef = useRef(onRefreshStats);
onRefreshStatsRef.current = onRefreshStats;
// Live-Sync WebSocket listener for tasks (registered once on mount)
useEffect(() => {
const handleWs = (e: any) => {
const msg = e.detail;
if (!msg || !msg.event) return;
if (msg.event === 'TASK_CREATED') {
const created = msg.payload as Thought;
if (
created &&
(created.status === 'converted' ||
created.kanbanStatus ||
created.taskListId ||
created.isCompleted !== undefined)
) {
setTasks((prev) => {
if (prev.some((t) => t.id === created.id)) return prev;
if (selectedListIdRef.current !== 'all' && created.taskListId !== selectedListIdRef.current) return prev;
return [created, ...prev];
});
onRefreshStatsRef.current();
}
} else if (msg.event === 'TASK_UPDATED') {
const updated = msg.payload as Thought;
if (updated && updated.id) {
setTasks((prev) => {
const exists = prev.some((t) => t.id === updated.id);
if (!exists) {
if (
(updated.status === 'converted' ||
updated.kanbanStatus ||
updated.taskListId ||
updated.isCompleted !== undefined) &&
(selectedListIdRef.current === 'all' || updated.taskListId === selectedListIdRef.current)
) {
return [updated, ...prev];
}
return prev;
}
if (selectedListIdRef.current !== 'all' && updated.taskListId !== selectedListIdRef.current) {
return prev.filter((t) => t.id !== updated.id);
}
return prev.map((t) => (t.id === updated.id ? updated : t));
});
onRefreshStatsRef.current();
}
} else if (msg.event === 'TASK_DELETED') {
const deletedId = typeof msg.payload === 'string' ? msg.payload : msg.payload?.id;
if (deletedId) {
setTasks((prev) => prev.filter((t) => t.id !== deletedId));
onRefreshStatsRef.current();
}
}
};
window.addEventListener('mindsift:ws', handleWs);
return () => window.removeEventListener('mindsift:ws', handleWs);
}, []);
const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId);
// Active Kanban Columns (from selected list, or global custom, or default)
const activeColumns: KanbanColumn[] = useMemo(() => {
if (
currentSelectedList &&
Array.isArray(currentSelectedList.columns) &&
currentSelectedList.columns.length > 0
) {
return currentSelectedList.columns;
}
return globalColumns;
}, [currentSelectedList, globalColumns]);
const safeTasks = useMemo(() => {
if (!Array.isArray(tasks)) return [];
const seen = new Set<string>();
const unique: Thought[] = [];
for (const t of tasks) {
if (t && t.id && !seen.has(t.id)) {
seen.add(t.id);
unique.push(t);
}
}
return unique;
}, [tasks]);
const detailTask = useMemo(() => {
if (!detailTaskId) return null;
return tasks.find((t) => t.id === detailTaskId) || null;
}, [detailTaskId, tasks]);
const safeTeams = Array.isArray(teams) ? teams : [];
const safeCollaborators = Array.isArray(collaborators) ? collaborators : [];
const safeDiscarded = Array.isArray(discarded) ? discarded : [];
// Available unique hashtags from current tasks
const allAvailableTags = useMemo(() => {
const set = new Set<string>();
for (const t of safeTasks) {
if (Array.isArray(t.tags)) {
for (const tg of t.tags) {
if (tg) set.add(tg);
}
}
}
return Array.from(set).sort();
}, [safeTasks]);
// Filtered tasks for List view
const openTasks = safeTasks.filter((t) => !t.isCompleted);
const doneTasks = safeTasks.filter((t) => t.isCompleted);
const filteredTasks = useMemo(() => {
let result = safeTasks;
if (filter === 'open') {
result = result.filter((t) => !t.isCompleted);
} else if (filter === 'done') {
result = result.filter((t) => t.isCompleted);
}
if (selectedTag) {
result = result.filter((t) => Array.isArray(t.tags) && t.tags.includes(selectedTag));
}
return [...result].sort((a, b) => {
if (sortBy === 'due') {
if (!a.dueDate && !b.dueDate) return 0;
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return a.dueDate.localeCompare(b.dueDate);
}
if (sortBy === 'priority') {
const pScore: Record<string, number> = { urgent: 3, high: 2, normal: 1, low: 0 };
const scoreA = pScore[a.priority] || 1;
const scoreB = pScore[b.priority] || 1;
return scoreB - scoreA;
}
if (sortBy === 'alpha') {
return a.content.localeCompare(b.content);
}
return b.createdAt.localeCompare(a.createdAt);
});
}, [safeTasks, filter, selectedTag, sortBy]);
// Tasks for Kanban board (shows all non-discarded matching tag)
const kanbanTasks = useMemo(() => {
let result = safeTasks;
if (selectedTag) {
result = result.filter((t) => Array.isArray(t.tags) && t.tags.includes(selectedTag));
}
return result;
}, [safeTasks, selectedTag]);
// Grouped tasks calculation
const groupedTasks = useMemo(() => {
if (groupBy === 'none') return null;
const groups: { key: string; title: string; count: number; tasks: Thought[] }[] = [];
if (groupBy === 'tag') {
const map = new Map<string, Thought[]>();
const withoutTag: Thought[] = [];
for (const t of filteredTasks) {
if (!t.tags || t.tags.length === 0) {
withoutTag.push(t);
} else {
for (const tg of t.tags) {
if (!map.has(tg)) map.set(tg, []);
map.get(tg)!.push(t);
}
}
}
Array.from(map.keys()).sort().forEach((tg) => {
const list = map.get(tg)!;
groups.push({ key: `tag:${tg}`, title: `#${tg}`, count: list.length, tasks: list });
});
if (withoutTag.length > 0) {
groups.push({ key: 'tag:none', title: 'Ohne Hashtag', count: withoutTag.length, tasks: withoutTag });
}
} else if (groupBy === 'status') {
activeColumns.forEach((col) => {
const list = filteredTasks.filter((t) => {
if (col.isDone) return t.isCompleted || t.kanbanStatus === col.id;
return !t.isCompleted && (t.kanbanStatus === col.id || (!t.kanbanStatus && col.id === 'todo'));
});
groups.push({ key: `status:${col.id}`, title: col.name, count: list.length, tasks: list });
});
} else if (groupBy === 'priority') {
const urgent = filteredTasks.filter((t) => t.priority === 'urgent');
const high = filteredTasks.filter((t) => t.priority === 'high');
const normal = filteredTasks.filter((t) => t.priority !== 'urgent' && t.priority !== 'high');
if (urgent.length > 0) groups.push({ key: 'p:urgent', title: '🔥 Dringend', count: urgent.length, tasks: urgent });
if (high.length > 0) groups.push({ key: 'p:high', title: '⚠️ Wichtig', count: high.length, tasks: high });
if (normal.length > 0) groups.push({ key: 'p:normal', title: 'Normal', count: normal.length, tasks: normal });
} else if (groupBy === 'due') {
const today = new Date().toISOString().split('T')[0];
const overdue = filteredTasks.filter((t) => t.dueDate && t.dueDate < today);
const dueToday = filteredTasks.filter((t) => t.dueDate === today);
const upcoming = filteredTasks.filter((t) => t.dueDate && t.dueDate > today);
const noDue = filteredTasks.filter((t) => !t.dueDate);
if (overdue.length > 0) groups.push({ key: 'due:overdue', title: '⚠️ Überfällig', count: overdue.length, tasks: overdue });
if (dueToday.length > 0) groups.push({ key: 'due:today', title: '📅 Heute fällig', count: dueToday.length, tasks: dueToday });
if (upcoming.length > 0) groups.push({ key: 'due:upcoming', title: 'Demnächst', count: upcoming.length, tasks: upcoming });
if (noDue.length > 0) groups.push({ key: 'due:none', title: 'Ohne Fälligkeitsdatum', count: noDue.length, tasks: noDue });
}
return groups;
}, [filteredTasks, groupBy, activeColumns]);
// --- TASK ACTIONS ---
const handleToggleDone = async (task: Thought) => {
triggerHaptic(15);
const nextState = !task.isCompleted;
const doneCol = activeColumns.find((c) => c.isDone) || activeColumns[activeColumns.length - 1];
const todoCol = activeColumns.find((c) => !c.isDone) || activeColumns[0];
const nextKanban = nextState ? (doneCol ? doneCol.id : 'done') : (todoCol ? todoCol.id : 'todo');
setTasks((prev) =>
prev.map((t) =>
t.id === task.id ? { ...t, isCompleted: nextState, kanbanStatus: nextKanban } : t
)
);
await api.updateThought(task.id, { isCompleted: nextState, kanbanStatus: nextKanban });
onRefreshStats();
};
const handleSetKanbanStatus = async (task: Thought, nextStatus: string) => {
triggerHaptic(15);
const targetCol = activeColumns.find((c) => c.id === nextStatus);
const isCompleted = targetCol ? !!targetCol.isDone : nextStatus === 'done';
setTasks((prev) =>
prev.map((t) => (t.id === task.id ? { ...t, kanbanStatus: nextStatus, isCompleted } : t))
);
await api.updateThought(task.id, { kanbanStatus: nextStatus, isCompleted });
onRefreshStats();
};
const handleSetPriority = async (task: Thought, nextPriority: string) => {
triggerHaptic(10);
const normalized = nextPriority === 'none' || nextPriority === '' ? 'normal' : nextPriority;
setTasks((prev) =>
prev.map((t) => (t.id === task.id ? { ...t, priority: normalized as any } : t))
);
await api.updateThought(task.id, { priority: normalized });
onRefreshStats();
};
const handleCyclePriority = async (task: Thought) => {
triggerHaptic(10);
const nextPriority =
task.priority === 'urgent'
? 'normal'
: task.priority === 'high'
? 'urgent'
: 'high';
await handleSetPriority(task, nextPriority);
};
const handleUpdateTaskTitle = async (task: Thought, newTitle: string) => {
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, content: newTitle } : t)));
await api.updateThought(task.id, { content: newTitle });
onRefreshStats();
};
const handleUpdateTaskDueDate = async (task: Thought, newDate: string) => {
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, dueDate: newDate } : t)));
await api.updateThought(task.id, { dueDate: newDate });
};
const handleUpdateTaskTaskList = async (task: Thought, newListId: string) => {
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, taskListId: newListId } : t)));
await api.updateThought(task.id, { taskListId: newListId });
if (selectedListId !== 'all' && selectedListId !== newListId) {
setTasks((prev) => prev.filter((t) => t.id !== task.id));
}
onRefreshStats();
};
const handleUpdateTaskNotes = async (task: Thought, newNotes: string) => {
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, notes: newNotes } : t)));
await api.updateThought(task.id, { notes: newNotes });
};
const handleUpdateTaskTags = async (task: Thought, newTags: string[]) => {
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, tags: newTags } : t)));
await api.updateThought(task.id, { tags: newTags });
onRefreshStats();
};
const handleToggleSubtask = async (task: Thought, subtaskId: string) => {
triggerHaptic(10);
const updatedSubtasks = (task.subtasks || []).map((s) =>
s.id === subtaskId ? { ...s, isCompleted: !s.isCompleted } : s
);
setTasks((prev) =>
prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t))
);
await api.updateThought(task.id, { subtasks: updatedSubtasks });
};
const handleAddSubtask = async (task: Thought, title: string) => {
triggerHaptic(10);
const newSub: SubTask = {
id: `st_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
title: title.trim(),
isCompleted: false,
createdAt: new Date().toISOString(),
};
const updatedSubtasks = [...(task.subtasks || []), newSub];
setTasks((prev) =>
prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t))
);
await api.updateThought(task.id, { subtasks: updatedSubtasks });
};
const handleDeleteSubtask = async (task: Thought, subtaskId: string) => {
triggerHaptic(10);
const updatedSubtasks = (task.subtasks || []).filter((s) => s.id !== subtaskId);
setTasks((prev) =>
prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t))
);
await api.updateThought(task.id, { subtasks: updatedSubtasks });
};
const handleConvertToDoc = async (task: Thought) => {
triggerHaptic(20);
const updated = await api.triageAction(task.id, 'keep', { convertTo: 'document' });
onRefreshStats();
if (updated.convertedRefId) {
onOpenDocument(updated.convertedRefId);
} else {
loadData();
}
};
const handleRestore = async (item: Thought) => {
triggerHaptic(15);
await api.triageAction(item.id, 'restore');
await loadData();
onRefreshStats();
};
const handleDeletePermanent = async (id: string) => {
triggerHaptic(20);
await api.deleteThought(id);
await loadData();
onRefreshStats();
};
const handlePurgeAllDiscarded = async () => {
if (!window.confirm('Alle verworfenen Gedanken endgültig löschen?')) return;
triggerHaptic(20);
await api.purgeDiscarded();
await loadData();
onRefreshStats();
};
const handleQuickAddTask = async (e: React.FormEvent) => {
e.preventDefault();
const title = quickTitle.trim();
if (!title) return;
triggerHaptic(10);
setQuickTitle('');
try {
const created = await api.captureThought(title, 'normal', 'manual', quickDueDate, {
target: 'task',
taskListId: selectedListId === 'all' ? '' : selectedListId,
});
setTasks((prev) => {
if (prev.some((t) => t.id === created.id)) return prev;
return [created, ...prev];
});
onRefreshStats();
} catch {
loadData();
}
};
const handleQuickAddTaskToColumn = async (columnId: string) => {
const title = prompt('Neue Aufgabe für diese Spalte:');
if (!title || !title.trim()) return;
triggerHaptic(10);
const targetCol = activeColumns.find((c) => c.id === columnId);
const isCompleted = targetCol ? !!targetCol.isDone : columnId === 'done';
try {
const created = await api.captureThought(title.trim(), 'normal', 'manual', undefined, {
target: 'task',
taskListId: selectedListId === 'all' ? '' : selectedListId,
});
if (created && created.id) {
const updated = await api.updateThought(created.id, { kanbanStatus: columnId, isCompleted });
setTasks((prev) => [updated, ...prev]);
onRefreshStats();
}
} catch {
loadData();
}
};
const handleCreateTaskList = async (e: React.FormEvent) => {
e.preventDefault();
const title = newListTitle.trim();
if (!title) return;
try {
let teamId: string | undefined = undefined;
let userId: string | undefined = undefined;
if (newListShareTarget.startsWith('team:')) {
teamId = newListShareTarget.replace('team:', '');
} else if (newListShareTarget.startsWith('user:')) {
userId = newListShareTarget.replace('user:', '');
}
const created = await api.createTaskList({
title,
icon: newListIcon,
teamId: teamId,
});
if (userId && created?.id) {
await api.addTaskListShare(created.id, { targetType: 'user', targetId: userId });
}
const freshLists = await api.getTaskLists();
setTaskLists(freshLists);
setSelectedListId(created.id);
setShowNewListModal(false);
setNewListTitle('');
setNewListShareTarget('');
} catch (err: any) {
alert(err.message || 'Fehler beim Erstellen der Aufgabenliste');
}
};
const handleDeleteTaskList = async (listId: string) => {
if (
!window.confirm(
'Möchten Sie diese Aufgabenliste wirklich löschen? Die enthaltenen Aufgaben bleiben erhalten.'
)
)
return;
try {
await api.deleteTaskList(listId);
setTaskLists((prev) => prev.filter((l) => l.id !== listId));
setSelectedListId('all');
} catch (err: any) {
alert(err.message);
}
};
const handleOpenShareModal = async (tl: TaskList) => {
setSharingTaskList(tl);
setSelectedShareTargetId('');
setSelectedShareUser(null);
setShowShareListModal(true);
try {
const [freshTeams, freshUsers, freshShares] = await Promise.all([
api.getUserTeams().catch(() => []),
api.getUsersDirectory ? api.getUsersDirectory().catch(() => api.getCollaborators().catch(() => [])) : api.getCollaborators().catch(() => []),
api.getTaskListShares(tl.id).catch(() => []),
]);
setTeams(freshTeams);
setCollaborators(freshUsers);
setSharingTaskList((prev) => (prev && prev.id === tl.id ? { ...prev, shares: freshShares } : prev));
} catch {
// ignore
}
};
const handleAddShare = async () => {
if (!sharingTaskList) return;
const targetId = addShareType === 'team' ? selectedShareTargetId : selectedShareUser?.id;
if (!targetId) return;
setShareLoading(true);
triggerHaptic(15);
try {
await api.addTaskListShare(sharingTaskList.id, {
targetType: addShareType,
targetId: targetId,
});
const [freshLists, freshShares] = await Promise.all([
api.getTaskLists(),
api.getTaskListShares(sharingTaskList.id).catch(() => []),
]);
setTaskLists(freshLists);
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
if (updated) {
setSharingTaskList({ ...updated, shares: freshShares });
}
setSelectedShareTargetId('');
setSelectedShareUser(null);
await loadData();
} catch (err: any) {
alert(err.message || 'Fehler beim Hinzufügen der Freigabe');
} finally {
setShareLoading(false);
}
};
const handleRemoveShare = async (shareId: string) => {
if (!sharingTaskList) return;
if (!window.confirm('Freigabe wirklich aufheben?')) return;
setShareLoading(true);
triggerHaptic(15);
try {
await api.removeTaskListShare(sharingTaskList.id, shareId);
const [freshLists, freshShares] = await Promise.all([
api.getTaskLists(),
api.getTaskListShares(sharingTaskList.id).catch(() => []),
]);
setTaskLists(freshLists);
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
if (updated) {
setSharingTaskList({ ...updated, shares: freshShares });
}
await loadData();
} catch (err: any) {
alert(err.message || 'Fehler beim Entfernen der Freigabe');
} finally {
setShareLoading(false);
}
};
const handleSaveColumns = async (newColumns: KanbanColumn[]) => {
triggerHaptic(15);
if (currentSelectedList && selectedListId !== 'all') {
const updated = await api.updateTaskList(currentSelectedList.id, { columns: newColumns });
setTaskLists((prev) => prev.map((l) => (l.id === updated.id ? updated : l)));
} else {
localStorage.setItem('mindsift:kanban_columns', JSON.stringify(newColumns));
setGlobalColumns(newColumns);
}
};
const renderTaskProgressBar = (task: Thought) => {
if (!task.subtasks || task.subtasks.length === 0) return null;
const total = task.subtasks.length;
const done = task.subtasks.filter((s) => s.isCompleted).length;
const percent = Math.round((done / total) * 100);
return (
<div
className="space-y-0.5 mt-0.5 mb-0.5 select-none"
title={`${done} von ${total} Unteraufgaben erledigt (${percent}%)`}
>
<div className="flex items-center justify-between text-[9px] text-muted font-medium">
<span className="flex items-center gap-1">
<CheckSquare className="w-2.5 h-2.5 text-accent" />
<span>
{done}/{total} Unteraufgaben
</span>
</span>
<span className={percent === 100 ? 'text-status-ok font-bold' : 'text-muted'}>
{percent}%
</span>
</div>
<div className="w-full h-1 bg-border rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-300 ${
percent === 100 ? 'bg-status-ok' : 'bg-accent'
}`}
style={{ width: `${percent}%` }}
/>
</div>
</div>
);
};
// Clean Task Row in List View
const renderTaskRow = (task: Thought) => {
const isCompleted = task.isCompleted;
return (
<div
key={task.id}
className={`px-3 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 ${
isCompleted ? 'opacity-65' : ''
}`}
>
<div className="flex items-center gap-3 min-w-0 flex-1">
{/* Checkbox */}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleToggleDone(task);
}}
className={`w-4 h-4 checkbox-square flex items-center justify-center flex-shrink-0 transition-colors border ${
isCompleted
? 'bg-status-ok border-status-ok text-on-accent'
: 'border-strong hover:border-accent text-transparent'
}`}
title={isCompleted ? 'Als offen markieren' : 'Als erledigt abhaken'}
>
<Check className="w-3 h-3 stroke-[3]" />
</button>
{/* Clickable Title & Metadata */}
<div
onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
}}
className="min-w-0 flex-1 cursor-pointer space-y-1"
>
<div className="flex items-center gap-2 flex-wrap">
<p
className={`text-sm break-words leading-tight ${
isCompleted ? 'line-through text-faint' : 'text-main font-medium'
}`}
>
{task.content}
</p>
</div>
{/* Subtasks Progress Bar */}
{renderTaskProgressBar(task)}
{/* Interactive Badges Bar */}
<div
className="flex flex-wrap items-center gap-1.5 pt-0.5 empty:hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Kanban status selector pill */}
<select
value={task.kanbanStatus || (isCompleted ? 'done' : 'todo')}
onChange={(e) => handleSetKanbanStatus(task, e.target.value)}
className="h-5 px-1.5 text-[10px] rounded-btn border border-border bg-panel text-muted hover:text-main cursor-pointer"
title="Status / Zustand ändern"
>
{activeColumns.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</select>
{/* Priority badge: click to cycle or clear */}
{task.priority === 'urgent' && (
<button
type="button"
onClick={() => handleCyclePriority(task)}
className="badge-accent text-[10px] cursor-pointer hover:opacity-85"
title="Klicken zum Ändern der Priorität (Dringend -> Normal)"
>
🔥 Dringend
</button>
)}
{task.priority === 'high' && (
<button
type="button"
onClick={() => handleCyclePriority(task)}
className="px-1.5 py-0.5 border border-status-warn text-status-warn rounded-btn text-[10px] font-medium cursor-pointer hover:opacity-85"
title="Klicken zum Ändern der Priorität (Wichtig -> Dringend)"
>
⚠️ Wichtig
</button>
)}
{/* Note indicator */}
{task.notes && (
<button
type="button"
onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
}}
className="inline-flex items-center gap-1 text-[10px] text-muted hover:text-main bg-canvas px-1.5 py-0.5 rounded border border-border"
title="Notiz anzeigen / bearbeiten"
>
<FileText className="w-2.5 h-2.5 text-accent" />
<span className="truncate max-w-[120px]">{task.notes}</span>
</button>
)}
{/* Due Date */}
{task.dueDate && (
<span className="inline-flex items-center gap-1 text-[10px] text-muted">
<Calendar className="w-2.5 h-2.5 text-accent" />
<span>{task.dueDate}</span>
</span>
)}
{/* Hashtag badges */}
{task.tags?.map((tag) => (
<button
key={tag}
type="button"
onClick={() => setSelectedTag(selectedTag === tag ? null : tag)}
className={`text-[10px] px-1.5 py-0.2 rounded-pill border transition-colors ${
selectedTag === tag
? 'bg-accent/15 border-accent text-accent font-semibold'
: 'badge-neutral hover:border-accent'
}`}
title={`Nach #${tag} filtern`}
>
#{tag}
</button>
))}
</div>
</div>
</div>
{/* Action Controls */}
<div className="flex items-center gap-1 flex-shrink-0">
<button
type="button"
onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
}}
className="text-faint hover:text-accent p-1.5 transition-colors"
title="Aufgabe bearbeiten"
>
<Edit3 className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => handleConvertToDoc(task)}
title="Als Notiz vertiefen"
className="text-faint hover:text-accent p-1.5 transition-colors hidden sm:block"
>
<FileText className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => handleDeletePermanent(task.id)}
title="Aufgabe löschen"
className="text-faint hover:text-status-err p-1.5 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
};
return (
<div className="w-full space-y-3">
{/* Top Header: Task Lists Tabs & View Mode Switcher */}
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-border pb-2">
{/* Task Lists selector tabs */}
<div className="flex flex-wrap items-center gap-1">
<button
onClick={() => setSelectedListId('all')}
className={`h-8 px-3 text-xs font-medium border rounded-btn transition-colors ${
selectedListId === 'all'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
Alle Aufgaben
</button>
{safeTaskLists.map((tl) => (
<div key={tl.id} className="relative group flex items-center">
<button
onClick={() => setSelectedListId(tl.id)}
className={`h-8 px-3 text-xs font-medium border rounded-btn flex items-center gap-1.5 transition-colors ${
selectedListId === tl.id
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<span>{tl.icon}</span>
<span>{tl.title}</span>
{tl.tasksCount !== undefined && tl.tasksCount > 0 && (
<span className="ml-1 badge-neutral">{tl.tasksCount}</span>
)}
{(() => {
const isShared = (tl.shares && tl.shares.length > 0) || Boolean(tl.teamId) || (tl.members && tl.members.length > 1);
if (!isShared) return null;
const otherMembers = (tl.members || []).filter((m) => m.userId !== currentUser?.id);
if (otherMembers.length > 0) {
return (
<div className="flex items-center -space-x-1 ml-0.5" title={`Geteilt (${tl.members?.length || 1} Mitglieder)`}>
{otherMembers.slice(0, 2).map((m) => (
<span key={m.userId} className="w-3.5 h-3.5 rounded-pill bg-accent/20 border border-panel text-[8px] font-bold text-accent flex items-center justify-center">
{getInitials(m.displayName || m.email)[0]}
</span>
))}
</div>
);
}
return (
<span title="Geteilt mit Team oder Benutzern">
<Users className="w-2.5 h-2.5 text-accent ml-0.5" />
</span>
);
})()}
</button>
{selectedListId === tl.id && (
<button
onClick={() => handleDeleteTaskList(tl.id)}
title="Liste löschen"
className="h-8 px-1.5 bg-panel border-y border-r border-accent text-faint hover:text-status-err"
>
<X className="w-3 h-3" />
</button>
)}
</div>
))}
<button
onClick={() => setShowNewListModal(true)}
className="h-8 px-2.5 bg-canvas hover:bg-panel border border-border border-dashed rounded-btn text-xs text-muted hover:text-main flex items-center gap-1 transition-colors"
title="Neue Aufgabenliste anlegen"
>
<Plus className="w-3.5 h-3.5" />
<span>Neue Liste</span>
</button>
</div>
{/* Right Header Section: Sharing Badge, Icon-Only Button & View Switcher */}
<div className="flex items-center gap-2">
{currentSelectedList && (() => {
const listOwnerId = currentSelectedList.ownerId || currentSelectedList.userId;
const isOwner = currentSelectedList.isOwner !== false && (!listOwnerId || listOwnerId === currentUser?.id);
const sharesCount = (currentSelectedList.shares?.length || 0) > 0
? currentSelectedList.shares.length
: (currentSelectedList.members?.filter((m) => m.userId !== listOwnerId).length || 0);
const isShared = sharesCount > 0 || Boolean(currentSelectedList.teamId) || ((currentSelectedList.members?.length || 0) > 1);
const ownerMember = currentSelectedList.members?.find((m) => m.role === 'owner' || m.userId === listOwnerId);
const ownerDisplayName = currentSelectedList.ownerName || ownerMember?.displayName || ownerMember?.email || 'Eigentümer';
return (
<div className="flex items-center gap-1.5">
{isShared && (
isOwner ? (
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="px-2 py-0.5 rounded text-[10px] font-semibold bg-accent/15 text-accent border border-accent/30 flex items-center gap-1 hover:bg-accent/25 hover:border-accent/50 transition-colors cursor-pointer"
title="Teilen-Menü öffnen: Geteilt mit Nutzern/Gruppen anzeigen & verwalten"
>
<Users className="w-2.5 h-2.5" />
<span>Geteilt ({sharesCount > 0 ? sharesCount : currentSelectedList.members?.length || 1})</span>
</button>
) : (
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="px-2 py-0.5 rounded text-[10px] font-medium border border-border text-muted flex items-center gap-1 bg-surface-subtle hover:bg-panel-elevated hover:text-main transition-colors cursor-pointer"
title="Teilen & Berechtigungen öffnen"
>
<Users className="w-2.5 h-2.5" />
<span>Geteilt von {ownerDisplayName}</span>
</button>
)
)}
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="btn-icon"
title="Aufgabenliste teilen und Berechtigungen"
>
<Share className="w-3.5 h-3.5" />
</button>
</div>
);
})()}
{/* View Switcher: List vs Kanban */}
<div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
<button
onClick={() => setViewMode('list')}
className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors border-r border-border ${
viewMode === 'list' ? 'bg-panel text-main font-medium' : 'text-muted hover:text-main'
}`}
title="Listenansicht"
>
<LayoutList className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Liste</span>
</button>
<button
onClick={() => setViewMode('kanban')}
className={`h-8 px-2.5 flex items-center gap-1.5 text-xs transition-colors ${
viewMode === 'kanban'
? 'bg-panel text-main font-medium'
: 'text-muted hover:text-main'
}`}
title="Kanban-Board"
>
<Columns className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Kanban</span>
</button>
</div>
</div>
</div>
{/* Quick Add Task Input */}
<form
onSubmit={handleQuickAddTask}
className="flex items-center gap-1.5 panel-surface p-1.5"
>
<Plus className="w-4 h-4 text-accent ml-1 flex-shrink-0" />
<input
type="text"
value={quickTitle}
onChange={(e) => setQuickTitle(e.target.value)}
placeholder={
currentSelectedList
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (Enter)`
: 'Neue Aufgabe anlegen… (Enter)'
}
className="flex-1 bg-transparent px-2 py-1 text-sm text-main placeholder-faint focus:outline-none"
/>
<div className="flex items-center gap-1">
<input
type="date"
value={quickDueDate}
onChange={(e) => setQuickDueDate(e.target.value)}
className="h-7 px-2 input-field text-xs"
title="Fälligkeitsdatum"
/>
<button
type="submit"
disabled={!quickTitle.trim()}
className="btn-primary h-7 px-3 text-xs disabled:opacity-40"
>
Hinzufügen
</button>
</div>
</form>
{/* Hashtag Filter & Grouping / Sorting Toolbar */}
<div className="flex flex-wrap items-center justify-between gap-2 pt-1 pb-0.5">
{/* Hashtag filter chips */}
<div className="flex items-center gap-1.5 overflow-x-auto max-w-full pb-1">
<span className="text-[11px] font-semibold text-muted flex items-center gap-1 flex-shrink-0">
<Hash className="w-3 h-3 text-accent" />
<span>Hashtags:</span>
</span>
<button
onClick={() => setSelectedTag(null)}
className={`h-6 px-2 text-[11px] rounded-pill border transition-colors flex-shrink-0 ${
selectedTag === null
? 'bg-panel border-accent text-accent font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
Alle ({safeTasks.length})
</button>
{allAvailableTags.map((tag) => {
const count = safeTasks.filter((t) => t.tags?.includes(tag)).length;
const isSelected = selectedTag === tag;
return (
<button
key={tag}
onClick={() => setSelectedTag(isSelected ? null : tag)}
className={`h-6 px-2 text-[11px] rounded-pill border transition-colors flex items-center gap-1 flex-shrink-0 ${
isSelected
? 'bg-accent/15 border-accent text-accent font-semibold'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<span>#{tag}</span>
<span className="text-[9px] opacity-70 font-mono">({count})</span>
</button>
);
})}
</div>
{/* Group By & Sort By Dropdowns & Manage Columns button */}
<div className="flex items-center gap-2 flex-shrink-0 ml-auto">
{viewMode === 'kanban' ? (
<button
onClick={() => setShowManageColumnsModal(true)}
className="btn-action h-7 px-2.5 text-xs flex items-center gap-1.5"
title="Kanban-Spalten hinzufügen, umbenennen oder neu anordnen"
>
<Columns className="w-3.5 h-3.5 text-accent" />
<span>Spalten anpassen</span>
</button>
) : (
<>
{/* Group By */}
<div className="flex items-center gap-1 text-xs text-muted">
<span className="text-[11px]">Gruppe:</span>
<select
value={groupBy}
onChange={(e) => setGroupBy(e.target.value as any)}
className="h-7 px-2 input-field text-xs bg-panel"
>
<option value="none">Keine</option>
<option value="tag">Nach Hashtag</option>
<option value="status">Nach Status</option>
<option value="priority">Nach Priorität</option>
<option value="due">Nach Fälligkeit</option>
</select>
</div>
{/* Sort By */}
<div className="flex items-center gap-1 text-xs text-muted">
<span className="text-[11px]">Sortierung:</span>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="h-7 px-2 input-field text-xs bg-panel"
>
<option value="created">Erstelldatum</option>
<option value="due">Fälligkeitsdatum</option>
<option value="priority">Priorität</option>
<option value="alpha">Titel (A-Z)</option>
</select>
</div>
</>
)}
</div>
</div>
{/* NEW LIST MODAL */}
{showNewListModal && (
<div className="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
<div className="w-full max-w-sm panel-surface shadow-panel p-4 space-y-3">
<div className="flex items-center justify-between border-b border-border pb-2">
<h3 className="text-sm font-semibold text-main flex items-center gap-1.5">
<Layers className="w-4 h-4 text-accent" />
<span>Neue Aufgabenliste erstellen</span>
</h3>
<button
onClick={() => setShowNewListModal(false)}
className="text-muted hover:text-main"
>
<X className="w-4 h-4" />
</button>
</div>
<form onSubmit={handleCreateTaskList} className="space-y-3">
<div>
<label className="block text-xs text-muted mb-1">Titel der Liste</label>
<div className="flex items-center gap-1.5">
<input
type="text"
value={newListIcon}
onChange={(e) => setNewListIcon(e.target.value)}
maxLength={2}
className="w-10 h-8 text-center input-field text-sm"
title="Icon (Emoji)"
/>
<input
type="text"
value={newListTitle}
onChange={(e) => setNewListTitle(e.target.value)}
placeholder="z.B. Homelab, Einkäufe, Steuern"
autoFocus
required
className="flex-1 h-8 input-field px-2.5 text-xs"
/>
</div>
</div>
<div>
<label className="block text-xs text-muted mb-1">
Freigabe (optional)
</label>
<select
value={newListShareTarget}
onChange={(e) => setNewListShareTarget(e.target.value)}
className="w-full h-8 input-field px-2 text-xs"
>
<option value="">🔒 Privat (nur für mich)</option>
{safeTeams.length > 0 && (
<optgroup label="Teams & Gruppen">
{safeTeams.map((tm) => (
<option key={tm.id} value={`team:${tm.id}`}>
👥 {tm.name}
</option>
))}
</optgroup>
)}
{safeCollaborators.length > 0 && (
<optgroup label="Einzelne Benutzer">
{safeCollaborators.map((u) => (
<option key={u.id} value={`user:${u.id}`}>
👤 {u.displayName || u.email}
</option>
))}
</optgroup>
)}
</select>
</div>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-border">
<button
type="button"
onClick={() => setShowNewListModal(false)}
className="btn-action h-7 px-3 text-xs"
>
Abbrechen
</button>
<button type="submit" className="btn-primary h-7 px-3 text-xs">
Erstellen
</button>
</div>
</form>
</div>
</div>
)}
{/* UNIFIED SHARE LIST MODAL */}
{showShareListModal && sharingTaskList && (
<UnifiedShareModal
isOpen={showShareListModal}
onClose={() => {
setShowShareListModal(false);
setSharingTaskList(null);
}}
title={`Aufgabenliste teilen: ${sharingTaskList.title}`}
resourceName={sharingTaskList.title}
resourceIcon={sharingTaskList.icon || '📋'}
isOwner={sharingTaskList.isOwner !== false}
currentUserId={currentUser?.id}
shares={(sharingTaskList.shares || []).map((sh) => ({
id: sh.id,
targetType: sh.targetType,
targetId: sh.targetId,
targetName: sh.targetName || (sh.targetType === 'team' ? 'Gruppe' : sh.email || 'Benutzer'),
targetEmail: sh.email,
permission: 'editor' as const,
role: 'editor',
createdAt: sh.createdAt,
}))}
teams={safeTeams}
collaborators={safeCollaborators}
allowRoleSelection={false}
onAddShare={async (targetType, targetId) => {
await api.addTaskListShare(sharingTaskList.id, {
targetType,
targetId,
});
const [freshLists, freshShares] = await Promise.all([
api.getTaskLists(),
api.getTaskListShares(sharingTaskList.id).catch(() => []),
]);
setTaskLists(freshLists);
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
if (updated) {
setSharingTaskList({ ...updated, shares: freshShares });
}
await loadData();
}}
onRemoveShare={async (shareId) => {
await api.removeTaskListShare(sharingTaskList.id, shareId);
const [freshLists, freshShares] = await Promise.all([
api.getTaskLists(),
api.getTaskListShares(sharingTaskList.id).catch(() => []),
]);
setTaskLists(freshLists);
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
if (updated) {
setSharingTaskList({ ...updated, shares: freshShares });
}
await loadData();
}}
/>
)}
{/* MANAGE COLUMNS MODAL */}
<ManageColumnsModal
isOpen={showManageColumnsModal}
onClose={() => setShowManageColumnsModal(false)}
initialColumns={activeColumns}
onSave={handleSaveColumns}
listTitle={currentSelectedList?.title || 'Alle Aufgaben'}
/>
{/* TASK DETAIL MODAL */}
<TaskDetailModal
task={detailTask}
isOpen={showDetailModal}
onClose={() => {
setShowDetailModal(false);
setDetailTaskId(null);
}}
activeColumns={activeColumns}
taskLists={safeTaskLists}
onUpdateTitle={handleUpdateTaskTitle}
onUpdateStatus={handleSetKanbanStatus}
onUpdatePriority={handleSetPriority}
onUpdateDueDate={handleUpdateTaskDueDate}
onUpdateTaskList={handleUpdateTaskTaskList}
onUpdateNotes={handleUpdateTaskNotes}
onUpdateTags={handleUpdateTaskTags}
onToggleSubtask={handleToggleSubtask}
onAddSubtask={handleAddSubtask}
onDeleteSubtask={handleDeleteSubtask}
onConvertToDoc={handleConvertToDoc}
onDeleteTask={handleDeletePermanent}
/>
{/* VIEW MODES: LIST OR KANBAN */}
{viewMode === 'kanban' ? (
/* DYNAMIC KANBAN BOARD */
<div className="space-y-2">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 pt-1">
{activeColumns.map((col, colIdx) => {
const colTasks = kanbanTasks.filter((t) => {
if (col.isDone) {
return t.isCompleted || t.kanbanStatus === col.id;
}
if (t.isCompleted) return false;
if (t.kanbanStatus) return t.kanbanStatus === col.id;
return col.id === 'todo';
});
return (
<div key={col.id} className="bg-canvas border border-border flex flex-col min-h-[420px]">
{/* Column Header */}
<div className="px-3 py-2 bg-panel border-b border-border flex items-center justify-between">
<span className="text-xs font-semibold text-main flex items-center gap-1.5">
<span
className={`w-2 h-2 rounded-full ${
col.isDone
? 'bg-status-ok'
: col.id === 'in_progress'
? 'bg-accent'
: col.id === 'waiting'
? 'bg-status-warn'
: 'bg-faint'
}`}
/>
<span>{col.name}</span>
</span>
<span className="badge-neutral font-mono">{colTasks.length}</span>
</div>
{/* Tasks List in Column */}
<div className="p-2 space-y-2 flex-1 overflow-y-auto max-h-[70vh]">
{colTasks.map((task) => (
<div
key={task.id}
onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
}}
className="panel-surface p-2.5 space-y-2 hover:border-strong transition-all cursor-pointer shadow-sm group"
>
<div className="flex items-start justify-between gap-2">
<p
className={`text-xs font-medium break-words flex-1 leading-snug ${
task.isCompleted ? 'line-through text-faint' : 'text-main'
}`}
>
{task.content}
</p>
<div className="flex items-center gap-1 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleConvertToDoc(task)}
title="Als Notiz vertiefen"
className="text-faint hover:text-accent p-0.5"
>
<FileText className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeletePermanent(task.id)}
title="Aufgabe löschen"
className="text-faint hover:text-status-err p-0.5"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
{/* Subtasks progress */}
{renderTaskProgressBar(task)}
{/* Notes preview badge */}
{task.notes && (
<div className="flex items-center gap-1 text-[10px] text-muted bg-canvas p-1 rounded border border-border truncate">
<FileText className="w-2.5 h-2.5 text-accent flex-shrink-0" />
<span className="truncate">{task.notes}</span>
</div>
)}
{/* Badges: Priority, Status, Tags */}
<div className="flex flex-wrap items-center gap-1 pt-0.5" onClick={(e) => e.stopPropagation()}>
{task.priority === 'urgent' && (
<button
type="button"
onClick={() => handleCyclePriority(task)}
className="badge-accent text-[9px] cursor-pointer hover:opacity-85"
title="Klicken zum Ändern der Priorität (Dringend -> Normal)"
>
🔥 Dringend
</button>
)}
{task.priority === 'high' && (
<button
type="button"
onClick={() => handleCyclePriority(task)}
className="px-1.5 py-0.2 border border-status-warn text-status-warn rounded-btn text-[9px] font-medium cursor-pointer hover:opacity-85"
title="Klicken zum Ändern der Priorität (Wichtig -> Dringend)"
>
⚠️ Wichtig
</button>
)}
{task.dueDate && (
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted">
<Calendar className="w-2.5 h-2.5" />
<span>{task.dueDate}</span>
</span>
)}
{task.tags?.map((tag) => (
<button
key={tag}
type="button"
onClick={() => setSelectedTag(selectedTag === tag ? null : tag)}
className={`text-[9px] px-1.5 py-0.2 rounded-pill border transition-colors ${
selectedTag === tag
? 'bg-accent/15 border-accent text-accent font-semibold'
: 'badge-neutral hover:border-accent'
}`}
>
#{tag}
</button>
))}
</div>
{/* Column Navigation controls: Move Left / Move Right */}
<div
className="flex items-center justify-between pt-1 border-t border-border text-[10px]"
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
disabled={colIdx === 0}
onClick={() => handleSetKanbanStatus(task, activeColumns[colIdx - 1].id)}
className="text-faint hover:text-main disabled:opacity-20 flex items-center gap-0.5"
title={colIdx > 0 ? `Nach "${activeColumns[colIdx - 1].name}"` : ''}
>
<ArrowLeft className="w-3 h-3" />
<span className="hidden sm:inline">{colIdx > 0 ? activeColumns[colIdx - 1].name : ''}</span>
</button>
<button
type="button"
disabled={colIdx === activeColumns.length - 1}
onClick={() => handleSetKanbanStatus(task, activeColumns[colIdx + 1].id)}
className="text-faint hover:text-main disabled:opacity-20 flex items-center gap-0.5"
title={colIdx < activeColumns.length - 1 ? `Nach "${activeColumns[colIdx + 1].name}"` : ''}
>
<span className="hidden sm:inline">{colIdx < activeColumns.length - 1 ? activeColumns[colIdx + 1].name : ''}</span>
<ArrowRight className="w-3 h-3" />
</button>
</div>
</div>
))}
{colTasks.length === 0 && (
<div className="p-4 text-center text-faint text-xs italic">
Keine Aufgaben in dieser Spalte
</div>
)}
</div>
{/* Quick Add Task to Column */}
<div className="p-2 border-t border-border bg-panel">
<button
type="button"
onClick={() => handleQuickAddTaskToColumn(col.id)}
className="w-full h-7 px-2 text-xs border border-dashed border-border rounded-btn text-muted hover:text-main hover:bg-canvas flex items-center justify-center gap-1 transition-colors"
>
<Plus className="w-3 h-3" />
<span>Aufgabe hinzufügen</span>
</button>
</div>
</div>
);
})}
</div>
</div>
) : (
/* STANDARD LIST VIEW (CLEAN & UNCLUTTERED) */
<div className="space-y-2">
{/* Subfilter tabs for List View */}
<div className="flex items-center justify-between gap-2">
<div className="flex items-center border border-border rounded-btn bg-canvas overflow-hidden">
<button
onClick={() => setFilter('open')}
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
filter === 'open' ? 'bg-panel text-main font-semibold' : 'text-muted hover:text-main'
}`}
>
Offen ({openTasks.length})
</button>
<button
onClick={() => setFilter('done')}
className={`h-8 px-3.5 text-xs font-medium border-r border-border transition-colors ${
filter === 'done' ? 'bg-panel text-main font-semibold' : 'text-muted hover:text-main'
}`}
>
Erledigt ({doneTasks.length})
</button>
<button
onClick={() => setFilter('discarded')}
className={`h-8 px-3.5 text-xs font-medium transition-colors ${
filter === 'discarded' ? 'bg-panel text-main font-semibold' : 'text-muted hover:text-main'
}`}
>
Papierkorb ({safeDiscarded.length})
</button>
</div>
{filter === 'discarded' && safeDiscarded.length > 0 && (
<button onClick={handlePurgeAllDiscarded} className="btn-action">
<Trash2 className="w-3.5 h-3.5" />
Papierkorb leeren
</button>
)}
</div>
{loading ? (
<div className="p-6 text-center text-faint text-sm">Lade Aufgaben…</div>
) : filter === 'discarded' ? (
<div className="panel-surface divide-y divide-border">
{safeDiscarded.length === 0 ? (
<div className="px-4 py-6 text-center text-muted text-sm">
Keine verworfenen Gedanken im Papierkorb.
</div>
) : (
safeDiscarded.map((item) => (
<div
key={item.id}
className="px-3 py-2 flex items-center justify-between gap-3 hover:bg-panel-hover"
>
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<Trash2 className="w-4 h-4 text-faint flex-shrink-0" />
<p className="text-sm text-muted line-through truncate">{item.content}</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button onClick={() => handleRestore(item)} className="btn-action">
<RotateCcw className="w-3 h-3 text-accent" /> Zurück
</button>
<button
onClick={() => handleDeletePermanent(item.id)}
className="btn-icon btn-icon-danger"
title="Endgültig löschen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
))
)}
</div>
) : (
<div className="space-y-3">
{groupedTasks ? (
/* GROUPED LIST */
groupedTasks.map((grp) => (
<div key={grp.key} className="panel-surface overflow-hidden">
<div className="px-3 py-1.5 bg-panel border-b border-border flex items-center justify-between">
<span className="text-xs font-semibold text-main">{grp.title}</span>
<span className="badge-neutral font-mono text-[10px]">{grp.count}</span>
</div>
<div>
{grp.tasks.map((task) => renderTaskRow(task))}
</div>
</div>
))
) : (
/* FLAT LIST */
<div className="panel-surface divide-y divide-border overflow-hidden">
{filteredTasks.length === 0 ? (
<div className="p-8 text-center text-muted text-sm space-y-1">
<p className="font-medium text-main">Keine Aufgaben gefunden</p>
<p className="text-xs text-faint">
{selectedTag
? `Keine Aufgaben mit Hashtag #${selectedTag}.`
: filter === 'done'
? 'Noch keine Aufgaben erledigt.'
: 'Erstelle oben eine neue Aufgabe.'}
</p>
</div>
) : (
filteredTasks.map((task) => renderTaskRow(task))
)}
</div>
)}
</div>
)}
</div>
)}
</div>
);
};