"use client"; import React, { useState, useEffect, useCallback, useRef } from "react"; import { useI18n } from "@/lib/i18n"; import { Task } from "./TaskList"; import { MarkdownNoteEditor } from "./MarkdownNoteEditor"; interface Props { task: Task; listId: string; listName?: string; onClose: () => void; onUpdate: (t: Task) => void; onDelete: () => void; isDemo?: boolean; onDemoUpdateTask?: (updated: Task) => void; onDemoDeleteTask?: (id: string) => void; } export function TaskDetail({ task, listId, listName, onClose, onUpdate, onDelete, isDemo = false, onDemoUpdateTask, onDemoDeleteTask, }: Props) { const { t, lang } = useI18n(); const [title, setTitle] = useState(task.title); const [note, setNote] = useState(task.note || ""); const [dueDate, setDueDate] = useState(task.dueDate ? task.dueDate.split("T")[0] : ""); const [priority, setPriority] = useState(task.priority); const [newSubtitle, setNewSubtitle] = useState(""); const [subtasks, setSubtasks] = useState(task.children || []); const [showSubtasks, setShowSubtasks] = useState(true); const [saving, setSaving] = useState(false); const saveTimer = useRef | null>(null); const currentTaskId = useRef(task.id); const priorityMap = [ { label: t("priorityNone"), color: "var(--text-tertiary)", icon: "" }, { label: t("priorityLow"), color: "var(--priority-low)", icon: "▼" }, { label: t("priorityMedium"), color: "var(--priority-medium)", icon: "▶" }, { label: t("priorityHigh"), color: "var(--priority-high)", icon: "▲" }, ]; // Sync state when switching task useEffect(() => { if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; } currentTaskId.current = task.id; setTitle(task.title); setNote(task.note || ""); setDueDate(task.dueDate ? task.dueDate.split("T")[0] : ""); setPriority(task.priority); setSubtasks(task.children || []); }, [task.id, task.title, task.note, task.dueDate, task.priority, task.children]); useEffect(() => { return () => { if (saveTimer.current) clearTimeout(saveTimer.current); }; }, []); const save = useCallback( async (taskId: string, data: Record) => { if (taskId !== currentTaskId.current) return; setSaving(true); if (isDemo) { const updatedTask: Task = { ...task, ...data, children: subtasks, updatedAt: new Date().toISOString(), } as Task; if (onDemoUpdateTask) onDemoUpdateTask(updatedTask); onUpdate(updatedTask); setSaving(false); return; } try { const res = await fetch(`/api/tasks/${taskId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (res.ok) { const updated = await res.json(); if (taskId === currentTaskId.current) { onUpdate({ ...updated, children: subtasks }); } } } catch (err) { console.error("[TaskDetail] save failed", err); } finally { if (taskId === currentTaskId.current) setSaving(false); } }, [isDemo, onDemoUpdateTask, onUpdate, subtasks, task] ); const debounceSave = useCallback( (overrides: Record) => { if (saveTimer.current) clearTimeout(saveTimer.current); const taskId = currentTaskId.current; saveTimer.current = setTimeout(() => save(taskId, overrides), 600); }, [save] ); const handleNoteChange = (newNote: string) => { setNote(newNote); debounceSave({ note: newNote }); }; const handleToggleCompleted = useCallback( async (newCompleted: boolean) => { if (isDemo) { const updatedTask: Task = { ...task, completed: newCompleted, completedAt: newCompleted ? new Date().toISOString() : null, children: subtasks, }; if (onDemoUpdateTask) onDemoUpdateTask(updatedTask); onUpdate(updatedTask); return; } try { const res = await fetch(`/api/tasks/${task.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ completed: newCompleted }), }); if (res.ok) { const updated = await res.json(); onUpdate({ ...updated, children: subtasks }); } } catch (err) { console.error("[TaskDetail] toggle failed", err); } }, [isDemo, onDemoUpdateTask, task, subtasks, onUpdate] ); const handleDelete = useCallback(async () => { if (!confirm(t("deleteTaskConfirm"))) return; if (isDemo) { if (onDemoDeleteTask) onDemoDeleteTask(task.id); onDelete(); return; } try { await fetch(`/api/tasks/${task.id}`, { method: "DELETE" }); onDelete(); } catch (err) { console.error("[TaskDetail] delete failed", err); } }, [t, isDemo, onDemoDeleteTask, task.id, onDelete]); const addSubtask = useCallback(async () => { const tTitle = newSubtitle.trim(); if (!tTitle) return; if (isDemo) { const newSub: Task = { id: "demo-sub-" + Date.now(), listId, parentId: task.id, title: tTitle, note: null, completed: false, completedAt: null, dueDate: null, priority: 0, sortOrder: subtasks.length, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), children: [], tags: [], }; const nextSubs = [...subtasks, newSub]; setSubtasks(nextSubs); setNewSubtitle(""); const updatedParent = { ...task, children: nextSubs }; if (onDemoUpdateTask) onDemoUpdateTask(updatedParent); onUpdate(updatedParent); return; } try { const res = await fetch("/api/tasks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: tTitle, listId, parentId: task.id }), }); if (res.ok) { const sub = await res.json(); setSubtasks((prev) => { const next = [...prev, sub]; onUpdate({ ...task, children: next }); return next; }); setNewSubtitle(""); } } catch (err) { console.error("[TaskDetail] addSubtask failed", err); } }, [newSubtitle, isDemo, listId, task, subtasks, onDemoUpdateTask, onUpdate]); const toggleSubtask = useCallback( async (sub: Task) => { const newCompleted = !sub.completed; if (isDemo) { const nextSubs = subtasks.map((s) => s.id === sub.id ? { ...s, completed: newCompleted, completedAt: newCompleted ? new Date().toISOString() : null } : s ); setSubtasks(nextSubs); const updatedParent = { ...task, children: nextSubs }; if (onDemoUpdateTask) onDemoUpdateTask(updatedParent); onUpdate(updatedParent); return; } try { const res = await fetch(`/api/tasks/${sub.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ completed: newCompleted }), }); if (res.ok) { const updated = await res.json(); setSubtasks((prev) => { const next = prev.map((s) => (s.id === sub.id ? updated : s)); onUpdate({ ...task, children: next }); return next; }); } } catch (err) { console.error("[TaskDetail] toggleSubtask failed", err); } }, [isDemo, subtasks, task, onDemoUpdateTask, onUpdate] ); const deleteSubtask = useCallback( async (id: string) => { if (isDemo) { const nextSubs = subtasks.filter((s) => s.id !== id); setSubtasks(nextSubs); const updatedParent = { ...task, children: nextSubs }; if (onDemoUpdateTask) onDemoUpdateTask(updatedParent); onUpdate(updatedParent); return; } try { await fetch(`/api/tasks/${id}`, { method: "DELETE" }); setSubtasks((prev) => { const next = prev.filter((s) => s.id !== id); onUpdate({ ...task, children: next }); return next; }); } catch (err) { console.error("[TaskDetail] deleteSubtask failed", err); } }, [isDemo, subtasks, task, onDemoUpdateTask, onUpdate] ); const completedCount = subtasks.filter((s) => s.completed).length; const progressPct = subtasks.length > 0 ? Math.round((completedCount / subtasks.length) * 100) : 0; return (