import React, { useState, useEffect, useCallback } from 'react'; import { X, Check, Calendar, Trash2, FileText, Plus, CheckSquare, Hash, AlertTriangle, Flame, Folder, Webhook, Lock, UserPlus, Repeat, Eye, EyeOff, } from 'lucide-react'; import { Thought, TaskList, TaskListMember, KanbanColumn, User, Recurrence } from '../../lib/types'; import { UserAvatar } from '../../components/UserAvatar'; import { UserAutocomplete } from '../UserAutocomplete'; import { api, triggerHaptic } from '../../lib/api'; import { MarkdownViewer } from '../../lib/markdown'; import { addDays, joinDue, splitDue } from '../../lib/due'; interface TaskDetailModalProps { task: Thought | null; isOpen: boolean; onClose: () => void; activeColumns: KanbanColumn[]; taskLists: TaskList[]; onUpdateTitle: (task: Thought, newTitle: string) => Promise; onUpdateStatus: (task: Thought, newStatus: string) => Promise; onUpdatePriority: (task: Thought, newPriority: string) => Promise; onUpdateDueDate: (task: Thought, newDueDate: string) => Promise; onUpdateRecurrence?: (task: Thought, recurrence: Recurrence) => Promise; onUpdateTaskList: (task: Thought, newListId: string) => Promise; onUpdateNotes: (task: Thought, newNotes: string) => Promise; onUpdateTags?: (task: Thought, newTags: string[]) => Promise; assignableMembers?: TaskListMember[]; // empty unless the task's list is shared onUpdateAssignees?: (task: Thought, assignees: string[]) => Promise; onToggleSubtask: (task: Thought, subtaskId: string) => Promise; onAddSubtask: (task: Thought, title: string) => Promise; onDeleteSubtask: (task: Thought, subtaskId: string) => Promise; onConvertToDoc: (task: Thought) => void; onOpenDocument?: (docId: string, blockId?: string) => void; onDeleteTask: (taskId: string) => void; readOnly?: boolean; // Task belongs to a list shared with "viewer" permission } // The wrapper decides whether the dialog is shown; the content holds all hooks, so their number never // changes between renders (hooks after an early return break React when isOpen flips) export const TaskDetailModal: React.FC = (props) => props.isOpen && props.task ? : null; const TaskDetailModalContent: React.FC = ({ task, onClose, activeColumns, taskLists, onUpdateTitle, onUpdateStatus, onUpdatePriority, onUpdateDueDate, onUpdateRecurrence, onUpdateTaskList, onUpdateNotes, onUpdateTags, assignableMembers = [], onUpdateAssignees, onToggleSubtask, onAddSubtask, onDeleteSubtask, onConvertToDoc, onOpenDocument, onDeleteTask, readOnly = false, }) => { const [titleDraft, setTitleDraft] = useState(task.content); const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingNotes, setIsEditingNotes] = useState(false); const [notesDraft, setNotesDraft] = useState(task.notes || ''); // The notes field grows with its text, so long notes scroll with the dialog instead of inside a small box const growNotes = useCallback((el: HTMLTextAreaElement | null) => { if (!el) return; el.style.height = 'auto'; el.style.height = `${el.scrollHeight + 2}px`; }, []); const [newSubtaskTitle, setNewSubtaskTitle] = useState(''); const [newTagDraft, setNewTagDraft] = useState(''); const [isWatching, setIsWatching] = useState(Boolean(task.isWatching)); const [watchBusy, setWatchBusy] = useState(false); useEffect(() => { setTitleDraft(task.content); setNotesDraft(task.notes || ''); setIsEditingTitle(false); setIsEditingNotes(false); setIsWatching(Boolean(task.isWatching)); }, [task.id, task.isWatching]); const handleToggleWatch = async () => { if (watchBusy) return; setWatchBusy(true); try { const res = isWatching ? await api.unwatchTask(task.id) : await api.watchTask(task.id); const next = Boolean(res.watching); setIsWatching(next); task.isWatching = next; triggerHaptic(10); } catch { // ignore } finally { setWatchBusy(false); } }; // Handle escape key to close useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (isEditingNotes) { setIsEditingNotes(false); } else if (isEditingTitle) { setIsEditingTitle(false); } else { onClose(); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose, isEditingNotes, isEditingTitle]); const handleTitleSubmit = async () => { const trimmed = titleDraft.trim(); if (!trimmed) { setTitleDraft(task.content); setIsEditingTitle(false); return; } if (trimmed !== task.content) { await onUpdateTitle(task, trimmed); } setIsEditingTitle(false); }; const handleNotesSubmit = async () => { await onUpdateNotes(task, notesDraft); setIsEditingNotes(false); }; const handleAddSubtaskSubmit = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = newSubtaskTitle.trim(); if (!trimmed) return; await onAddSubtask(task, trimmed); setNewSubtaskTitle(''); }; const handleAddTagSubmit = async (e: React.FormEvent) => { e.preventDefault(); const cleaned = newTagDraft.trim().replace(/^#+/, '').toLowerCase(); if (!cleaned) return; const currentTags = Array.isArray(task.tags) ? [...task.tags] : []; if (!currentTags.includes(cleaned)) { const updated = [...currentTags, cleaned]; if (onUpdateTags) { await onUpdateTags(task, updated); } else { await onUpdateTitle(task, `${task.content} #${cleaned}`); } } setNewTagDraft(''); }; const handleRemoveTag = async (tagToRemove: string) => { const currentTags = Array.isArray(task.tags) ? [...task.tags] : []; const updated = currentTags.filter((t) => t !== tagToRemove); if (onUpdateTags) { await onUpdateTags(task, updated); } }; const dueParts = splitDue(task.dueDate); const handleQuickDueDate = (daysOffset: number | null) => { if (daysOffset === null) { onUpdateDueDate(task, ''); return; } // Keep a chosen time when only the day changes onUpdateDueDate(task, joinDue(addDays(daysOffset), splitDue(task.dueDate).time)); }; const subtasks = Array.isArray(task.subtasks) ? task.subtasks : []; const completedSubtasks = subtasks.filter((s) => s.isCompleted).length; const subtasksPercent = subtasks.length > 0 ? Math.round((completedSubtasks / subtasks.length) * 100) : 0; return (
e.stopPropagation()} > {/* Top Header */}
{readOnly ? 'Aufgabe ansehen' : task.isCompleted ? 'Erledigte Aufgabe' : 'Aufgabe bearbeiten'} {readOnly && ( Nur Lesen )} {task.source?.toLowerCase() === 'webhook' && ( Webhook )}
{(task.convertedType === 'document_block' || task.docId) ? ( ) : !readOnly && ( )} {!readOnly && ( )}
{/* Scrollable Body (all controls disabled for read-only shares) */} {/* The scroll container is a div: a scrolling
is unreliable in mobile browsers */}
{/* Task Title */}
{isEditingTitle ? (