import { useEffect, useState } from 'react' import { X, Trash2, Plus, Bell, Repeat, CalendarClock, FolderInput, Tag, FolderKanban, UserPlus, Send, ChevronDown, Pin } from 'lucide-react' import type { TaskDetail as TDetail, Priority, RecurRule } from '../types' import { PRIORITIES, PRIORITY_LABELS, RECUR_RULES, RECUR_LABELS } from '../types' import { fetchTask, updateTask, deleteTask, addItem, updateItem, deleteItem, addComment, tagTask, untagTask, reallocateTask, fetchProjectMembers, } from '../api' import { useLists } from '../context/ListsContext' import { useCategories } from '../context/CategoriesContext' import { useProjects } from '../context/ProjectsContext' import { useProjectStatuses } from '../hooks/useProjectStatuses' import { useAuth } from './AuthGate' import { can } from '../types' import { Check_, formatDateTime, toLocalInput, UserPicker } from './shared' export default function TaskDetail({ taskId, onChange, onClose }: { taskId: number; onChange: () => void; onClose: () => void }) { const { user } = useAuth() const { lists } = useLists() const { categories } = useCategories() const { projects } = useProjects() const [task, setTask] = useState(null) const { statuses } = useProjectStatuses(task?.project_id ?? null) const [error, setError] = useState(null) const [title, setTitle] = useState('') const [notes, setNotes] = useState('') const [newItem, setNewItem] = useState('') const [comment, setComment] = useState('') const [tagEmail, setTagEmail] = useState('') const [assignEmail, setAssignEmail] = useState('') const [reallocateEmail, setReallocateEmail] = useState('') const [sharingOpen, setSharingOpen] = useState(false) // Project 'editor' members can edit a task's content/status without // owning it (see loadEditable on the backend) — not resolvable from the // task payload alone, so check membership once the task's project is known. const [editorViaProject, setEditorViaProject] = useState(false) useEffect(() => { fetchTask(taskId).then(t => { setTask(t); setTitle(t.title); setNotes(t.notes ?? '') }) .catch(e => setError(e.message)) }, [taskId]) useEffect(() => { if (!task?.project_id || task.owner_id === user.email) { setEditorViaProject(false); return } fetchProjectMembers(task.project_id) .then(members => setEditorViaProject(members.some(m => m.user_email === user.email && m.role === 'editor'))) .catch(() => setEditorViaProject(false)) }, [task?.project_id, task?.owner_id, user.email]) if (error) return (
{error}
) if (!task) return
Loading…
const isOwner = task.owner_id === user.email // Content/status fields; list/project/assignment stay owner-only (see the // matching 403 on the backend). const canEdit = isOwner || editorViaProject // Patch task fields, merge the result locally, and notify the parent view. async function patch(fields: Record) { const updated = await updateTask(taskId, fields).catch(e => { setError(e.message); throw e }) setTask(t => t ? { ...t, ...updated } : t) setError(null) onChange() } const toLocalIso = (v: string) => v ? new Date(v).toISOString() : '' async function toggleDone() { const meta = statuses.find(s => s.value === task!.status) const target = meta?.is_done ? statuses.find(s => !s.is_done) : statuses.find(s => s.is_done) if (target) await patch({ status: target.value }) } async function addChecklistItem() { if (!newItem.trim()) return const item = await addItem(taskId, newItem.trim()) setTask(t => t ? { ...t, items: [...t.items, item] } : t) setNewItem('') onChange() } async function toggleItem(id: number, is_done: boolean) { await updateItem(id, { is_done }) setTask(t => t ? { ...t, items: t.items.map(i => i.id === id ? { ...i, is_done } : i) } : t) } async function removeItem(id: number) { await deleteItem(id) setTask(t => t ? { ...t, items: t.items.filter(i => i.id !== id) } : t) } async function postComment() { if (!comment.trim()) return const c = await addComment(taskId, comment.trim()) setTask(t => t ? { ...t, comments: [...t.comments, c] } : t) setComment('') } async function removeTask() { if (!confirm('Delete this task?')) return await deleteTask(taskId) onChange() onClose() } async function addTag() { if (!tagEmail.trim()) return await tagTask(taskId, tagEmail.trim()) setTask(t => t ? { ...t, tags: [...t.tags, { user_email: tagEmail.trim(), tagged_by: user.email, created_at: new Date().toISOString() }] } : t) setTagEmail('') } async function removeTag(email: string) { await untagTask(taskId, email) setTask(t => t ? { ...t, tags: t.tags.filter(tg => tg.user_email !== email) } : t) } async function assignTo() { if (!assignEmail.trim()) return // Once handed off, the task moves off the current owner's board entirely // (loadVisible no longer matches them) — close rather than leave the // drawer showing a task they can no longer reopen. await updateTask(taskId, { assigned_to: assignEmail.trim() }).catch(e => { setError(e.message); throw e }) setAssignEmail('') onChange() onClose() } async function reallocateTo() { if (!reallocateEmail.trim()) return await reallocateTask(taskId, reallocateEmail.trim()).catch(e => { setError(e.message); throw e }) setReallocateEmail('') onChange() onClose() } const doneItems = task.items.filter(i => i.is_done).length const currentStatus = statuses.find(s => s.value === task.status) return ( {/* Title + completion */}