import { useCallback, useEffect, useState } from "react"; import { useConsole } from "../context"; import { api, shortId } from "../api/client"; import { paths } from "../api/paths"; import { Card, Bar, DataList } from "../components/ui"; // Project todos: Muse-style checklist for a project, not a task // queue. Five statuses (pending / in_progress / blocked / // completed / cancelled), priority, optional linked task id and // due time; completed_at is set/cleared by the server on status // change. This section fetches directly (like the automation // panel's create/refresh pattern) instead of going through the // store, so no existing store hook has to change. type Todo = { id: string; project_id: string; title: string; description: string; status: string; priority: string; task_id: string | null; due_at: number | null; sort_order: number | null; created_by: string; created_at: number; updated_at: number; completed_at: number | null; }; const STATUSES = ["pending", "in_progress", "blocked", "completed", "cancelled"]; const STATUS_LABEL: Record = { pending: "待办", in_progress: "进行中", blocked: "受阻", completed: "已完成", cancelled: "已取消", }; const PRIORITIES = ["low", "medium", "high"]; const PRIORITY_LABEL: Record = { low: "低", medium: "中", high: "高" }; function fmtDue(due: number | null): string { if (!due) return ""; return new Date(due * 1000).toLocaleString(); } export default function TodosSection() { const ctx = useConsole(); const [todos, setTodos] = useState([]); const [counts, setCounts] = useState>({}); const [total, setTotal] = useState(0); const [msg, setMsg] = useState(""); const [err, setErr] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [q, setQ] = useState(""); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [priority, setPriority] = useState("medium"); const [taskId, setTaskId] = useState(""); const [due, setDue] = useState(""); const load = useCallback(async () => { if (!ctx.projectId) { setTodos([]); setCounts({}); setTotal(0); return; } try { let u = paths.projectTodos(ctx.projectId); const params = new URLSearchParams(); if (statusFilter) params.set("status", statusFilter); if (q.trim()) params.set("q", q.trim()); const qs = params.toString(); if (qs) u += "?" + qs; const d = await api<{ todos: Todo[]; counts: Record; total: number }>(u); setTodos(d.todos || []); setCounts(d.counts || {}); setTotal(d.total || 0); setErr(""); } catch (e: any) { setErr("待办加载失败:" + (e?.message || String(e))); } }, [ctx.projectId, statusFilter, q]); useEffect(() => { void load(); }, [load]); const create = async () => { if (!ctx.projectId) { setMsg("先选项目"); return; } if (!title.trim()) { setMsg("标题必填"); return; } setMsg(""); try { const body: Record = { title: title.trim(), description, priority }; if (taskId.trim()) body.task_id = taskId.trim(); if (due) body.due_at = Math.floor(new Date(due).getTime() / 1000); await api(paths.projectTodos(ctx.projectId), "POST", body); setTitle(""); setDescription(""); setTaskId(""); setDue(""); setMsg("已创建"); await load(); } catch (e: any) { setMsg("创建失败:" + (e?.message || String(e))); } }; const patch = async (t: Todo, fields: Record) => { if (!ctx.projectId) return; try { await api(paths.projectTodo(ctx.projectId, t.id), "PATCH", fields); await load(); } catch (e: any) { setErr("待办更新失败:" + (e?.message || String(e))); } }; const remove = async (t: Todo) => { if (!ctx.projectId) return; if (!window.confirm("删除待办“" + t.title + "”?删除后不可恢复(取消请改状态为已取消)。")) return; try { await api(paths.projectTodo(ctx.projectId, t.id), "DELETE"); await load(); } catch (e: any) { setErr("待办删除失败:" + (e?.message || String(e))); } }; return (
{ctx.projectId ? "共 " + total + " 项 | " + STATUSES.map((s) => STATUS_LABEL[s] + " " + (counts[s] || 0)).join(" | ") : "先选项目"}
{err ?
{err}
: null} setQ(e.target.value)} /> setTitle(e.target.value)} />