import React, { useEffect, useState, useRef, useMemo } from 'react'; import { Check, Plus, Calendar, Trash2, RotateCcw, FileText, LayoutList, Columns, ArrowRight, ArrowLeft, Edit3, X, Layers, Share2, Users, CheckSquare, } from 'lucide-react'; import { TaskList, Thought, Space, SubTask } from '../../lib/types'; import { api, triggerHaptic } from '../../lib/api'; 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 { onRefreshStats: () => void; onOpenDocument: (docId: string) => void; refreshTrigger?: number; } export const TasksView: React.FC = ({ 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 [spaces, setSpaces] = useState([]); const [discarded, setDiscarded] = useState([]); 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 [newListSpaceId, setNewListSpaceId] = useState(''); // Share TaskList modal state const [showShareListModal, setShowShareListModal] = useState(false); const [sharingTaskList, setSharingTaskList] = useState(null); const [shareTargetSpaceId, setShareTargetSpaceId] = useState(''); // Active notes inline edit ID const [activeNotesTaskId, setActiveNotesTaskId] = useState(null); const [notesDraft, setNotesDraft] = useState(''); // Subtask creation state const [addingSubtaskTaskId, setAddingSubtaskTaskId] = useState(null); const [subtaskDraft, setSubtaskDraft] = useState(''); const loadData = async () => { setLoading(true); try { const [allTasks, allLists, allSpaces, allDiscarded] = await Promise.all([ api.getTasks(selectedListId === 'all' ? undefined : selectedListId).catch(() => []), api.getTaskLists().catch(() => []), api.getSpaces().catch(() => []), api.getThoughts('discarded').catch(() => []), ]); setTasks(Array.isArray(allTasks) ? allTasks : []); setTaskLists(Array.isArray(allLists) ? allLists : []); setSpaces(Array.isArray(allSpaces) ? allSpaces : []); 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 activeNotesTaskIdRef = useRef(activeNotesTaskId); activeNotesTaskIdRef.current = activeNotesTaskId; 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) => { if (t.id !== updated.id) return t; return { ...updated, notes: activeNotesTaskIdRef.current === updated.id ? t.notes : updated.notes, }; }); }); 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 handleToggleDone = async (task: Thought) => { triggerHaptic(15); const nextState = !task.isCompleted; const nextKanban = nextState ? 'done' : '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 handleKanbanMove = async (task: Thought, nextStatus: 'todo' | 'in_progress' | 'done') => { triggerHaptic(15); const isCompleted = 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 handleDueDateChange = 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 handleSaveNotes = async (task: Thought) => { setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, notes: notesDraft } : t))); await api.updateThought(task.id, { notes: notesDraft }); setActiveNotesTaskId(null); }; const handleAddSubtask = async (task: Thought, title: string) => { const trimmed = title.trim(); if (!trimmed) return; triggerHaptic(10); const newSub: SubTask = { id: `sub_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, title: trimmed, isCompleted: false, createdAt: new Date().toISOString(), }; const updatedSubtasks = [...(task.subtasks || []), newSub]; setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, subtasks: updatedSubtasks } : t)) ); setSubtaskDraft(''); await api.updateThought(task.id, { subtasks: updatedSubtasks }); }; 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 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 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}%
); }; const renderSubtaskList = (task: Thought) => { const hasSubtasks = task.subtasks && task.subtasks.length > 0; const isAdding = addingSubtaskTaskId === task.id; if (!hasSubtasks && !isAdding) return null; return (
{hasSubtasks && (
{task.subtasks!.map((sub) => (
))}
)} {isAdding && (
{ e.preventDefault(); handleAddSubtask(task, subtaskDraft); }} className="flex items-center gap-1 pt-1" > setSubtaskDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Escape') { setAddingSubtaskTaskId(null); setSubtaskDraft(''); } }} placeholder="Neue Unteraufgabe… (Enter)" autoFocus className="flex-1 h-6 px-1.5 text-xs bg-canvas border border-accent rounded-btn text-main placeholder-faint focus:outline-none" />
)}
); }; 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 () => { 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 handleCreateTaskList = async (e: React.FormEvent) => { e.preventDefault(); const title = newListTitle.trim(); if (!title) return; try { const created = await api.createTaskList({ title, icon: newListIcon, spaceId: newListSpaceId, }); setTaskLists((prev) => [...prev, created]); setSelectedListId(created.id); setShowNewListModal(false); setNewListTitle(''); } 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 = (tl: TaskList) => { setSharingTaskList(tl); setShareTargetSpaceId(tl.spaceId || ''); setShowShareListModal(true); }; const handleSaveShareList = async () => { if (!sharingTaskList) return; triggerHaptic(15); try { const updated = await api.updateTaskList(sharingTaskList.id, { spaceId: shareTargetSpaceId, }); setTaskLists((prev) => prev.map((l) => (l.id === updated.id ? { ...l, ...updated } : l)) ); setShowShareListModal(false); setSharingTaskList(null); await loadData(); } catch (err: any) { alert(err.message || 'Fehler beim Speichern der Freigabe'); } }; 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 safeTaskLists = Array.isArray(taskLists) ? taskLists : []; const safeSpaces = Array.isArray(spaces) ? spaces : []; const safeDiscarded = Array.isArray(discarded) ? discarded : []; const openTasks = safeTasks.filter((t) => !t.isCompleted); const doneTasks = safeTasks.filter((t) => t.isCompleted); const todoKanban = safeTasks.filter( (t) => (!t.kanbanStatus || t.kanbanStatus === 'todo') && !t.isCompleted ); const inProgressKanban = safeTasks.filter( (t) => t.kanbanStatus === 'in_progress' && !t.isCompleted ); const doneKanban = safeTasks.filter((t) => t.kanbanStatus === 'done' || t.isCompleted); const currentSelectedList = safeTaskLists.find((l) => l.id === selectedListId); return (
{/* Top Header: Task Lists Tabs & View Mode Switcher */}
{/* Task Lists selector tabs */}
{safeTaskLists.map((tl) => (
{selectedListId === tl.id && ( )}
))}
{/* Right Header Section: Sharing, Avatar Stack & View Switcher */}
{currentSelectedList && (
{/* Avatar Stack in the corner */} {currentSelectedList.members && currentSelectedList.members.length > 0 ? (
handleOpenShareModal(currentSelectedList)} className="flex items-center -space-x-1.5 cursor-pointer hover:opacity-90 transition-opacity pl-0.5" title={`Geteilt mit ${currentSelectedList.members.length} Mitgliedern (${currentSelectedList.spaceName || 'Team'}). Klick zum Verwalten.`} > {currentSelectedList.members.slice(0, 3).map((m) => (
{getInitials(m.displayName || m.email)}
))} {currentSelectedList.members.length > 3 && (
+{currentSelectedList.members.length - 3}
)}
) : (
handleOpenShareModal(currentSelectedList)} className="flex items-center cursor-pointer pl-0.5" title="Privat (nur du). Klick zum Teilen." >
đź”’
)}
)} {/* View Switcher: List vs Kanban */}
{/* Quick Add Task Input in active list */}
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" />
setQuickDueDate(e.target.value)} className="h-7 px-2 input-field text-xs" title="Fälligkeitsdatum" />
{/* 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" />
{safeSpaces.length > 0 && (
)}
)} {/* SHARE LIST MODAL */} {showShareListModal && sharingTaskList && (

Liste teilen: {sharingTaskList.title}

{/* Members preview */} {(() => { const selectedSpace = safeSpaces.find((s) => s.id === shareTargetSpaceId); const members = selectedSpace?.members || []; if (members.length > 0) { return (

Personen mit Zugriff ({members.length}):

{members.map((m) => (
{getInitials(m.displayName || m.email)}

{m.displayName || m.email}

{m.email}

{m.role === 'owner' ? 'EigentĂĽmer' : m.role === 'editor' ? 'Bearbeiter' : 'Mitglied'}
))}
); } else if (shareTargetSpaceId) { return (

Keine Mitglieder in diesem Bereich.

); } else { return (

Diese Aufgabenliste ist privat.

Wähle oben einen Familien- oder Team-Space aus, um die Liste und alle enthaltenen Aufgaben automatisch mit den Mitgliedern zu teilen.

); } })()}
)} {/* VIEW MODES: LIST OR KANBAN */} {viewMode === 'kanban' ? ( /* KANBAN BOARD (3 COLUMNS) */
{/* COLUMN 1: TODO */}
Zu erledigen {todoKanban.length}
{todoKanban.map((task) => (

{task.content}

{/* Subtasks Progress Bar on Title */} {renderTaskProgressBar(task)} {/* Notes Preview / Inline Editor */} {activeNotesTaskId === task.id ? (