import { useContext, useEffect, useState } from "react"; import Modal from "@moduletrace/twui/elements/Modal"; import H2 from "@moduletrace/twui/layout/H2"; import Span from "@moduletrace/twui/layout/Span"; import Stack from "@moduletrace/twui/layout/Stack"; import Button from "@moduletrace/twui/layout/Button"; import Row from "@moduletrace/twui/layout/Row"; import { ListChecks } from "lucide-react"; import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi"; import type { APIResponseObject } from "@moduletrace/bunext/types"; import type { BUN_SQLITE_COBBLE_WORKSPACES } from "@/db/types/db"; import type { BotWithLastMessage, TaskDetail, TaskWithNames } from "@/src/types"; import TaskDetailPanel from "@/src/components/general/task-detail-panel"; import TaskForm from "@/src/components/general/task-form"; import TaskListItem from "@/src/components/general/task-list-item"; import { AppContext } from "@/src/pages/__root"; import canManageCrew from "@/src/utils/rbac/can-manage-crew"; import useBotTasks from "../(hooks)/use-bot-tasks"; import HeaderMenuItem from "./header-menu-item"; type Props = { bot: BotWithLastMessage; workspace?: BUN_SQLITE_COBBLE_WORKSPACES | null; /** * A row in the conversation header's menu instead of an icon beside it. */ menu?: boolean; on_open?: () => void; }; /** * The tasks this conversation works on: a bot's own list, or in a workspace * the workspace's shared one. Open it from the header menu; the list loads * then, so the page's props stay small. A crew manager adds, moves, edits * and deletes; a task opens to show its notes and files. */ export default function BotTasksModal({ bot, workspace, menu, on_open }: Props) { const { pageProps } = useContext(AppContext); const [open, setOpen] = useState(false); const [adding, set_adding] = useState(false); const [open_task, set_open_task] = useState(null); const [busy, set_busy] = useState(false); const [status, set_status] = useState(); const can_manage = canManageCrew({ user_types: pageProps?.user_types }); const { tasks, setTasks, loading, error, reload } = useBotTasks({ bot_id: Number(bot.id), workspace_id: workspace?.id ? Number(workspace.id) : null, enabled: open, }); const name = bot.name || "This bot"; const title = workspace?.id ? `${workspace.name || "Workspace"} tasks` : `${name}'s tasks`; useEffect(() => { if (open) on_open?.(); }, [open]); const replace = ({ task }: { task: TaskWithNames }) => setTasks((prev) => [task, ...prev.filter((item) => item.id != task.id)]); const move = async ({ task, next }: { task: TaskWithNames; next: string }) => { if (busy) return; set_busy(true); set_status(undefined); const res = await fetchApi, APIResponseObject>( "/api/admin/tasks/save-task", { method: "POST", body: { task_id: Number(task.id), status: next } }, ); set_busy(false); if (!res.success || !res.singleRes) { set_status(res.msg || "That didn't work."); return; } replace({ task: res.singleRes }); }; const show = async ({ task }: { task: TaskWithNames }) => { set_status(undefined); /** * The list already has the task; the detail endpoint serves its * notes and files. */ const detail = await fetchApi<{}, APIResponseObject>( `/api/admin/tasks/get-task?task_id=${task.id}`, { method: "GET" }, ).catch(() => null); if (!detail?.success || !detail.singleRes) { set_status(detail?.msg || "Couldn't open the task."); return; } set_open_task(detail.singleRes); }; return ( ) : ( ) } targetWrapperProps={{ className: menu ? "w-full" : "shrink-0" }} open={open} setOpen={(next) => { setOpen(next); if (!next) { set_open_task(null); set_adding(false); } }} className="max-w-xl bg-paper p-6" >

{open_task ? open_task.title || "Task" : title}

{open_task ? ( ) : can_manage ? ( ) : null}
{!open_task ? ( {workspace?.id ? `The shared to-do list of ${workspace.name || "this workspace"}. Every bot here sees it, and they keep it up to date as they work.` : `What ${name} is working through, and what it's waiting on.`} ) : null}
{open_task ? ( { set_status(msg); /** * Handed to another bot, it's on that bot's list now. */ if (!workspace?.id && saved.bot_id != bot.id) { setTasks((prev) => prev.filter((item) => item.id != saved.id)); set_open_task(null); return; } set_open_task(saved); replace({ task: saved }); }} onDeleted={() => { setTasks((prev) => prev.filter((item) => item.id != open_task.id)); set_open_task(null); set_status("Task deleted."); }} /> ) : adding ? ( set_adding(false)} onSaved={(saved, msg) => { replace({ task: saved }); set_adding(false); set_status(msg); }} /> ) : tasks.length ? ( tasks.map((task) => ( show({ task }).catch(() => {})} onStatusChange={(next) => move({ task, next })} /> )) ) : ( {loading ? "Loading tasks..." : error || "No tasks yet."} )} {status ? ( {status} ) : null}
); }