import React, { useEffect, useState, useRef, useMemo } from 'react'; import { Check, Plus, Calendar, Trash2, RotateCcw, FileText, LayoutList, Columns, ArrowRight, ArrowLeft, Edit3, X, Layers, Users, CheckSquare, User as UserIcon, Lock, Hash, AlertTriangle, Flame, Settings, ChevronDown, Folder, Filter, Webhook, } from 'lucide-react'; import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn, PriorityLevel } 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'; import { useMobileActions } from '../../context/MobileActionContext'; 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 = ({ currentUser, onRefreshStats, onOpenDocument, refreshTrigger = 0, }) => { const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list'); const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open'); const [tasks, setTasks] = useState([]); const [taskLists, setTaskLists] = useState([]); const [selectedListId, setSelectedListId] = useState('all'); const [teams, setTeams] = useState([]); const [collaborators, setCollaborators] = useState([]); const [discarded, setDiscarded] = useState([]); const [loading, setLoading] = useState(true); // Drag and Drop state const [draggingTaskId, setDraggingTaskId] = useState(null); const [dragOverTaskTarget, setDragOverTaskTarget] = useState<{ taskId: string; position: 'before' | 'after' } | null>(null); const [dragOverKanbanCol, setDragOverKanbanCol] = useState(null); // Quick Task Add state const [quickTitle, setQuickTitle] = useState(''); const [quickDueDate, setQuickDueDate] = useState(''); const quickInputRef = useRef(null); const { setGroups, setPrimaryAction } = useMobileActions(); // Mobile task creation & filter modal state const [showMobileCreateModal, setShowMobileCreateModal] = useState(false); const [mobileCreateTitle, setMobileCreateTitle] = useState(''); const [mobileCreateDueDate, setMobileCreateDueDate] = useState(''); const [mobileCreatePriority, setMobileCreatePriority] = useState('normal'); const [mobileCreateListId, setMobileCreateListId] = useState(''); const [showFilterModal, setShowFilterModal] = useState(false); // New TaskList modal state const [showNewListModal, setShowNewListModal] = useState(false); const [newListTitle, setNewListTitle] = useState(''); const [newListIcon, setNewListIcon] = useState('📋'); // Share TaskList modal state const [showShareListModal, setShowShareListModal] = useState(false); const [sharingTaskList, setSharingTaskList] = useState(null); const [addShareType, setAddShareType] = useState<'team' | 'user'>('team'); const [selectedShareTargetId, setSelectedShareTargetId] = useState(''); const [selectedShareUser, setSelectedShareUser] = useState(null); const [shareLoading, setShareLoading] = useState(false); // Detail modal state const [detailTaskId, setDetailTaskId] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); // Manage columns modal state const [showManageColumnsModal, setShowManageColumnsModal] = useState(false); const [globalColumns, setGlobalColumns] = useState(() => { 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(null); const [groupBy, setGroupBy] = useState<'none' | 'tag' | 'status' | 'priority' | 'due'>('none'); const [sortBy, setSortBy] = useState<'custom' | 'created' | 'due' | 'priority' | 'alpha'>('custom'); 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(); } } else if (msg.event === 'TASK_REORDERED') { loadData(); } }; 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(); 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(); 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)); } if (sortBy === 'custom') { return [...result].sort((a, b) => { const orderA = a.sortOrder ?? 0; const orderB = b.sortOrder ?? 0; if (orderA !== orderB) return orderA - orderB; return b.createdAt.localeCompare(a.createdAt); }); } 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 = { 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(); 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 handleMobileCreateTask = async (e: React.FormEvent) => { e.preventDefault(); const title = mobileCreateTitle.trim(); if (!title) return; triggerHaptic(12); try { const created = await api.captureThought(title, mobileCreatePriority, 'manual', mobileCreateDueDate || undefined, { target: 'task', taskListId: mobileCreateListId || (selectedListId === 'all' ? '' : selectedListId), }); setTasks((prev) => { if (prev.some((t) => t.id === created.id)) return prev; return [created, ...prev]; }); setMobileCreateTitle(''); setMobileCreateDueDate(''); setMobileCreatePriority('normal'); setShowMobileCreateModal(false); 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 handleTaskDragStart = (e: React.DragEvent, task: Thought) => { e.stopPropagation(); e.dataTransfer.setData('application/mindsift-task-id', task.id); e.dataTransfer.setData('text/plain', task.id); e.dataTransfer.effectAllowed = 'move'; setDraggingTaskId(task.id); }; const handleTaskDragOver = (e: React.DragEvent, targetTaskId: string) => { e.preventDefault(); e.stopPropagation(); if (!draggingTaskId || draggingTaskId === targetTaskId) return; const rect = e.currentTarget.getBoundingClientRect(); const position = e.clientY - rect.top < rect.height / 2 ? 'before' : 'after'; if ( !dragOverTaskTarget || dragOverTaskTarget.taskId !== targetTaskId || dragOverTaskTarget.position !== position ) { setDragOverTaskTarget({ taskId: targetTaskId, position }); } }; const handleTaskDragLeave = (e: React.DragEvent, targetTaskId: string) => { e.stopPropagation(); if (dragOverTaskTarget?.taskId === targetTaskId) { setDragOverTaskTarget(null); } }; const handleDropOnTask = async (e: React.DragEvent, targetTask: Thought, newKanbanCol?: string) => { e.preventDefault(); e.stopPropagation(); const sourceId = e.dataTransfer.getData('application/mindsift-task-id') || e.dataTransfer.getData('text/plain') || draggingTaskId; const position = dragOverTaskTarget?.position || 'after'; setDraggingTaskId(null); setDragOverTaskTarget(null); setDragOverKanbanCol(null); if (!sourceId || sourceId === targetTask.id) return; triggerHaptic(15); const sourceTask = tasks.find((t) => t.id === sourceId); if (!sourceTask) return; const remainingTasks = tasks.filter((t) => t.id !== sourceId); const targetIdx = remainingTasks.findIndex((t) => t.id === targetTask.id); if (targetIdx === -1) return; const insertIdx = position === 'before' ? targetIdx : targetIdx + 1; const updatedSourceTask: Thought = { ...sourceTask, kanbanStatus: newKanbanCol || targetTask.kanbanStatus || sourceTask.kanbanStatus, isCompleted: newKanbanCol ? newKanbanCol === 'done' : targetTask.isCompleted !== undefined ? targetTask.isCompleted : sourceTask.isCompleted, }; const newTasks = [...remainingTasks]; newTasks.splice(insertIdx, 0, updatedSourceTask); const updatedWithSort = newTasks.map((t, idx) => ({ ...t, sortOrder: idx, })); setTasks(updatedWithSort); setSortBy('custom'); const reorderPayload = updatedWithSort.map((t) => ({ id: t.id, sortOrder: t.sortOrder ?? 0, kanbanStatus: t.kanbanStatus, })); try { await api.reorderTasks( reorderPayload, selectedListId !== 'all' ? selectedListId : undefined ); } catch { loadData(); } }; const handleDropOnKanbanCol = async (e: React.DragEvent, colId: string) => { e.preventDefault(); e.stopPropagation(); const sourceId = e.dataTransfer.getData('application/mindsift-task-id') || e.dataTransfer.getData('text/plain') || draggingTaskId; setDraggingTaskId(null); setDragOverTaskTarget(null); setDragOverKanbanCol(null); if (!sourceId) return; triggerHaptic(15); const sourceTask = tasks.find((t) => t.id === sourceId); if (!sourceTask) return; const isDone = colId === 'done'; const updatedSourceTask: Thought = { ...sourceTask, kanbanStatus: colId, isCompleted: isDone, }; const remainingTasks = tasks.filter((t) => t.id !== sourceId); const newTasks = [updatedSourceTask, ...remainingTasks]; const updatedWithSort = newTasks.map((t, idx) => ({ ...t, sortOrder: idx, })); setTasks(updatedWithSort); setSortBy('custom'); const reorderPayload = updatedWithSort.map((t) => ({ id: t.id, sortOrder: t.sortOrder ?? 0, kanbanStatus: t.kanbanStatus, })); try { await api.reorderTasks( reorderPayload, selectedListId !== 'all' ? selectedListId : undefined ); } catch { loadData(); } }; const handleCreateTaskList = async (e: React.FormEvent) => { e.preventDefault(); const title = newListTitle.trim(); if (!title) return; try { const created = await api.createTaskList({ title, icon: newListIcon, }); const freshLists = await api.getTaskLists(); setTaskLists(freshLists); setSelectedListId(created.id); setShowNewListModal(false); setNewListTitle(''); setNewListIcon('📋'); } 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); } }; useEffect(() => { setPrimaryAction(() => { setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId); setShowMobileCreateModal(true); }); const primaryItems: any[] = [ { id: 'new-task', label: 'Neue Aufgabe erstellen', icon: , variant: 'accent', onClick: () => { setMobileCreateListId(selectedListId === 'all' ? '' : selectedListId); setShowMobileCreateModal(true); }, }, ]; const filterSortActiveCount = (selectedTag ? 1 : 0) + (groupBy !== 'none' ? 1 : 0) + (sortBy !== 'custom' ? 1 : 0); const filterItems: any[] = [ { id: 'tasks-filter-modal', label: 'Filter, Hashtags & Sortierung', icon: , badge: filterSortActiveCount > 0 ? `${filterSortActiveCount} aktiv` : undefined, active: filterSortActiveCount > 0, onClick: () => setShowFilterModal(true), }, ]; const listManagementItems: any[] = [ { id: 'new-task-list', label: 'Neue Aufgabenliste anlegen', icon: , onClick: () => setShowNewListModal(true), }, ]; if (currentSelectedList) { listManagementItems.push({ id: 'share-task-list', label: `Liste „${currentSelectedList.title}“ teilen`, icon: , onClick: () => handleOpenShareModal(currentSelectedList), }); listManagementItems.push({ id: 'delete-task-list', label: `Liste „${currentSelectedList.title}“ löschen`, icon: , variant: 'danger', onClick: () => handleDeleteTaskList(currentSelectedList.id), }); } const viewItems: any[] = [ { id: 'filter-open', label: 'Offene Aufgaben (Liste)', icon: , badge: openTasks.length, active: viewMode === 'list' && filter === 'open', onClick: () => { setViewMode('list'); setFilter('open'); }, }, { id: 'filter-done', label: 'Erledigte Aufgaben', icon: , badge: doneTasks.length, active: viewMode === 'list' && filter === 'done', onClick: () => { setViewMode('list'); setFilter('done'); }, }, { id: 'filter-discarded', label: 'Papierkorb', icon: , badge: safeDiscarded.length, active: viewMode === 'list' && filter === 'discarded', onClick: () => { setViewMode('list'); setFilter('discarded'); }, }, { id: 'view-kanban', label: 'Kanban-Board Ansicht', icon: , badge: kanbanTasks.length, active: viewMode === 'kanban', onClick: () => setViewMode('kanban'), }, ]; if (viewMode === 'kanban') { viewItems.push({ id: 'manage-columns', label: 'Kanban-Spalten verwalten', icon: , onClick: () => setShowManageColumnsModal(true), }); } const listItems: any[] = [ { id: 'list-all', label: 'Alle Aufgaben', icon: , active: selectedListId === 'all', onClick: () => setSelectedListId('all'), }, ...safeTaskLists.map((tl) => ({ id: `list-${tl.id}`, label: `${tl.icon || '📋'} ${tl.title}`, badge: tl.tasksCount || undefined, active: selectedListId === tl.id, onClick: () => setSelectedListId(tl.id), })), ]; setGroups([ { id: 'tasks-primary', items: primaryItems, }, { id: 'tasks-filters', title: 'Filter & Sortierung', items: filterItems, }, { id: 'tasks-list-management', title: 'Listen-Verwaltung', items: listManagementItems, }, { id: 'tasks-views', title: 'Ansicht & Status', items: viewItems, }, { id: 'tasks-lists', title: 'Aufgabenlisten wechseln', items: listItems, }, ]); }, [ viewMode, filter, selectedListId, selectedTag, groupBy, sortBy, openTasks.length, doneTasks.length, safeDiscarded.length, kanbanTasks.length, safeTaskLists, currentSelectedList, setPrimaryAction, setGroups, ]); 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 (
{done}/{total} Unteraufgaben {percent}%
); }; // Clean Task Row in List View const renderTaskRow = (task: Thought) => { const isCompleted = task.isCompleted; const isDragging = draggingTaskId === task.id; const isTarget = dragOverTaskTarget?.taskId === task.id; return (
handleTaskDragStart(e, task)} onDragEnd={() => { setDraggingTaskId(null); setDragOverTaskTarget(null); setDragOverKanbanCol(null); }} onDragOver={(e) => handleTaskDragOver(e, task.id)} onDragLeave={(e) => handleTaskDragLeave(e, task.id)} onDrop={(e) => handleDropOnTask(e, task)} onClick={() => { setDetailTaskId(task.id); setShowDetailModal(true); }} className={`px-3.5 py-3.5 sm:px-3 sm: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 relative cursor-pointer cursor-grab active:cursor-grabbing select-none ${ isCompleted ? 'opacity-65' : '' } ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`} > {/* Drop insertion line indicator */} {isTarget && dragOverTaskTarget.position === 'before' && (
)} {isTarget && dragOverTaskTarget.position === 'after' && (
)}
{/* Checkbox (larger touch target on mobile) */} {/* Clickable Title & Metadata */}
{ setDetailTaskId(task.id); setShowDetailModal(true); }} className="min-w-0 flex-1 cursor-pointer space-y-1" >

{task.content}

{/* Subtasks Progress Bar */} {renderTaskProgressBar(task)} {/* Interactive Badges Bar */}
e.stopPropagation()} > {/* Kanban status selector pill (Desktop only - on mobile done via TaskDetailModal) */} {/* Priority badge: click to cycle or clear */} {task.priority === 'urgent' && ( )} {task.priority === 'high' && ( )} {task.source?.toLowerCase() === 'webhook' && ( Webhook )} {/* Note indicator */} {task.notes && ( )} {/* Due Date */} {task.dueDate && ( {task.dueDate} )} {/* Hashtag badges */} {task.tags?.map((tag) => ( ))}
{/* Action Controls (Desktop only - on mobile, tapping the task opens Detail Modal) */}
e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
); }; return (
{/* Mobile Compact Context Bar (replaces cluttered button rows; options live in MindSift Logo Menu) */}
{currentSelectedList ? `${currentSelectedList.icon} ${currentSelectedList.title}` : 'đź“‹ Alle Aufgaben'} {viewMode === 'kanban' ? 'Kanban' : filter === 'open' ? `Offen (${openTasks.length})` : filter === 'done' ? `Erledigt (${doneTasks.length})` : `Papierkorb (${safeDiscarded.length})`}
{currentSelectedList && ( )}
{/* Top Header: Task Lists Tabs & View Mode Switcher (Desktop) */}
{/* Task Lists selector tabs */}
{safeTaskLists.map((tl) => (
{selectedListId === tl.id && ( )}
))}
{/* Right Header Section: Sharing Badge, Icon-Only Button & View Switcher */}
{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 (
{isShared && ( isOwner ? ( ) : ( ) )}
); })()} {/* View Switcher: List vs Kanban */}
{/* Quick Add Task Input (Desktop only - on mobile, tapping the bottom '+' FAB opens mobile create sheet) */}
setQuickTitle(e.target.value)} placeholder={ currentSelectedList ? `Aufgabe zu "${currentSelectedList.title}" hinzufügen…` : 'Neue Aufgabe anlegen…' } className="flex-1 bg-transparent px-2 py-1.5 sm:py-1 text-sm text-main placeholder-faint focus:outline-none min-w-0" />
setQuickDueDate(e.target.value)} className="hidden sm:block h-7 px-2 input-field text-xs" title="Fälligkeitsdatum" />
{/* Mobile Active Filter Badge Banner (minimal and clean) */} {(selectedTag || groupBy !== 'none' || sortBy !== 'custom') && (
{selectedTag && ( #{selectedTag} )} {groupBy !== 'none' && ( Gruppe: {groupBy} )} {sortBy !== 'custom' && ( Sortiert: {sortBy} )}
)} {/* Hashtag Filter & Grouping / Sorting Toolbar (Desktop only - on mobile accessible via Logo Menu Filter & Sort) */}
{/* Hashtag filter chips */}
Hashtags: {allAvailableTags.map((tag) => { const count = safeTasks.filter((t) => t.tags?.includes(tag)).length; const isSelected = selectedTag === tag; return ( ); })}
{/* Group By & Sort By Dropdowns & Manage Columns button */}
{viewMode === 'kanban' ? ( ) : ( <> {/* Group By */}
Gruppe:
{/* Sort By */}
Sortierung:
)}
{/* MOBILE CREATE TASK BOTTOM SHEET MODAL */} {showMobileCreateModal && (
setShowMobileCreateModal(false)} >
e.stopPropagation()} >

Neue Aufgabe anlegen

setMobileCreateTitle(e.target.value)} placeholder="Was ist zu tun?…" className="w-full input-field px-3 py-2 text-sm" />
{/* List Selector (if lists exist) */} {safeTaskLists.length > 0 && (
)} {/* Priority - 4 Buttons */}
{(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => { const config: Record = { low: { label: 'Niedrig', activeClass: 'bg-canvas border-strong text-main font-semibold shadow-xs' }, normal: { label: 'Normal', activeClass: 'bg-accent/15 border-accent text-accent font-semibold shadow-xs' }, high: { label: 'Wichtig', icon: , activeClass: 'bg-status-warn/15 border-status-warn text-status-warn font-semibold shadow-xs' }, urgent: { label: 'Dringend', icon: , activeClass: 'bg-status-err/15 border-status-err text-status-err font-semibold shadow-xs' }, }; const active = mobileCreatePriority === p; const item = config[p]; return ( ); })}
{/* Due Date */}
setMobileCreateDueDate(e.target.value)} className="w-full input-field px-3 py-2 text-xs" />
)} {/* MOBILE FILTER & SORT MODAL */} {showFilterModal && (
setShowFilterModal(false)} >
e.stopPropagation()} >

Filter & Sortierung

{/* Hashtag Filters */}
Nach Hashtags filtern
{allAvailableTags.map((tag) => { const count = safeTasks.filter((t) => t.tags?.includes(tag)).length; const isSelected = selectedTag === tag; return ( ); })}
{/* Grouping */} {viewMode !== 'kanban' && (
Gruppierung
{[ { id: 'none', label: 'Keine' }, { id: 'tag', label: 'Nach Hashtag' }, { id: 'status', label: 'Nach Status' }, { id: 'priority', label: 'Nach Priorität' }, { id: 'due', label: 'Nach Fälligkeit' }, ].map((g) => ( ))}
)} {/* Sorting */} {viewMode !== 'kanban' && (
Sortierung
{[ { id: 'custom', label: 'Manuell (Drag & Drop)' }, { id: 'created', label: 'Erstelldatum' }, { id: 'due', label: 'Fälligkeitsdatum' }, { id: 'priority', label: 'Priorität' }, { id: 'alpha', label: 'Alphabetisch' }, ].map((s) => ( ))}
)}
)} {/* NEW LIST MODAL */} {showNewListModal && (

Neue Aufgabenliste erstellen

setNewListIcon(e.target.value)} maxLength={2} className="w-10 h-8 text-center input-field text-sm" title="Icon (Emoji)" /> 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" />
)} {/* UNIFIED SHARE LIST MODAL */} {showShareListModal && sharingTaskList && ( { 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, avatarUrl: sh.avatarUrl, 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 */} setShowManageColumnsModal(false)} initialColumns={activeColumns} onSave={handleSaveColumns} listTitle={currentSelectedList?.title || 'Alle Aufgaben'} /> {/* TASK DETAIL MODAL */} { 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 */
{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 (
{ e.preventDefault(); if (draggingTaskId) setDragOverKanbanCol(col.id); }} onDragLeave={() => { if (dragOverKanbanCol === col.id) setDragOverKanbanCol(null); }} onDrop={(e) => handleDropOnKanbanCol(e, col.id)} className={`bg-canvas border border-border flex flex-col min-h-[420px] transition-all ${ dragOverKanbanCol === col.id ? 'ring-2 ring-accent/60 bg-accent/5' : '' }`} > {/* Column Header */}
{col.name} {colTasks.length}
{/* Tasks List in Column */}
{colTasks.map((task) => { const isDragging = draggingTaskId === task.id; const isTarget = dragOverTaskTarget?.taskId === task.id; return (
handleTaskDragStart(e, task)} onDragEnd={() => { setDraggingTaskId(null); setDragOverTaskTarget(null); setDragOverKanbanCol(null); }} onDragOver={(e) => handleTaskDragOver(e, task.id)} onDragLeave={(e) => handleTaskDragLeave(e, task.id)} onDrop={(e) => handleDropOnTask(e, task, col.id)} onClick={() => { setDetailTaskId(task.id); setShowDetailModal(true); }} className={`panel-surface p-2.5 space-y-2 hover:border-strong transition-all cursor-grab active:cursor-grabbing shadow-sm group relative ${ isDragging ? 'opacity-30 border border-dashed border-accent/40' : '' }`} > {/* Drop insertion line indicator */} {isTarget && dragOverTaskTarget.position === 'before' && (
)} {isTarget && dragOverTaskTarget.position === 'after' && (
)}

{task.content}

e.stopPropagation()}>
{/* Subtasks progress */} {renderTaskProgressBar(task)} {/* Notes preview badge */} {task.notes && (
{task.notes}
)} {/* Badges: Priority, Status, Tags */}
e.stopPropagation()}> {task.priority === 'urgent' && ( )} {task.priority === 'high' && ( )} {task.source?.toLowerCase() === 'webhook' && ( Webhook )} {task.dueDate && ( {task.dueDate} )} {task.tags?.map((tag) => ( ))}
{/* Column Navigation controls: Move Left / Move Right */}
e.stopPropagation()} >
); })} {colTasks.length === 0 && (
Keine Aufgaben in dieser Spalte
)}
{/* Quick Add Task to Column */}
); })}
) : ( /* STANDARD LIST VIEW (CLEAN & UNCLUTTERED) */
{/* Subfilter tabs for List View (Desktop; on mobile controlled via MindSift Logo Menu) */}
{filter === 'discarded' && safeDiscarded.length > 0 && ( )}
{loading ? (
Lade Aufgaben…
) : filter === 'discarded' ? (
{safeDiscarded.length === 0 ? (
Keine verworfenen Gedanken im Papierkorb.
) : ( safeDiscarded.map((item) => (

{item.content}

)) )}
) : (
{groupedTasks ? ( /* GROUPED LIST */ groupedTasks.map((grp) => (
{grp.title} {grp.count}
{grp.tasks.map((task) => renderTaskRow(task))}
)) ) : ( /* FLAT LIST */
{filteredTasks.length === 0 ? (

Keine Aufgaben gefunden

{selectedTag ? `Keine Aufgaben mit Hashtag #${selectedTag}.` : filter === 'done' ? 'Noch keine Aufgaben erledigt.' : 'Erstelle oben eine neue Aufgabe.'}

) : ( filteredTasks.map((task) => renderTaskRow(task)) )}
)}
)}
)}
); };