import { useEffect, useMemo, useState, type FormEvent } from "react"; import { CalendarClock, Check, CirclePlay, ExternalLink, ListChecks, Plus, RotateCcw, Search, XCircle } from "lucide-react"; import { Link } from "react-router"; import { FormGrid, Button, DateTimeField, Dialog, DismissibleAlert, DocumentationHelpLink, FormField, FilterBar, SegmentedControl, SelectionList, SelectionListItem, StatePanel, StatusBadge, WorkspaceActionBar, WorkspaceFrame, WorkspaceLayout, hasScope, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { createTask, listWork, transitionTask, type TaskCreatePayload, type WorkItem, type WorkPriority, type WorkStatus } from "../../api/tasks"; type StatusView = "active" | "completed" | "all"; const ACTIVE_STATUSES: WorkStatus[] = ["open", "in_progress", "deferred", "blocked"]; const CLOSED_STATUSES: WorkStatus[] = ["completed", "cancelled"]; const ALL_STATUSES: WorkStatus[] = [...ACTIVE_STATUSES, ...CLOSED_STATUSES]; const DOCUMENTATION = { contextId: "tasks.page.inbox", topicId: "tasks.work-inbox", documentationType: "user" as const }; export default function TasksPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) { const [items, setItems] = useState([]); const [selectedKey, setSelectedKey] = useState(""); const [statusView, setStatusView] = useState("active"); const [searchDraft, setSearchDraft] = useState(""); const [query, setQuery] = useState(""); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [diagnostics, setDiagnostics] = useState([]); const [createOpen, setCreateOpen] = useState(false); const [deferOpen, setDeferOpen] = useState(false); const [deferredUntil, setDeferredUntil] = useState(""); const canWrite = hasScope(auth, "tasks:item:write"); const selected = useMemo( () => items.find((item) => workKey(item) === selectedKey) ?? items[0] ?? null, [items, selectedKey] ); useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, statusView, query]); async function load() { setLoading(true); setError(""); try { const response = await listWork(settings, { statuses: statusView === "active" ? ACTIVE_STATUSES : statusView === "completed" ? CLOSED_STATUSES : ALL_STATUSES, q: query, limit: 500 }); setItems(response.items); setDiagnostics(response.diagnostics.map((item) => `${item.owner_module}: ${item.message}`)); setSelectedKey((current) => response.items.some((item) => workKey(item) === current) ? current : response.items[0] ? workKey(response.items[0]) : ""); } catch (reason) { setError(errorMessage(reason)); } finally { setLoading(false); } } async function runAction(action: "start" | "complete" | "defer" | "reopen" | "cancel") { if (!selected || selected.provider_id !== "tasks.explicit") return; setBusy(true); setError(""); try { const next = await transitionTask( settings, selected, action, action === "defer" ? localDateTimeToIso(deferredUntil) : null ); setItems((current) => current.map((item) => item.id === next.id ? next : item)); setDeferOpen(false); setDeferredUntil(""); await load(); } catch (reason) { setError(errorMessage(reason)); } finally { setBusy(false); } } function submitSearch(event: FormEvent) { event.preventDefault(); setQuery(searchDraft.trim()); } return ( void load(), loading: loading || busy, label: "i18n:govoplan-tasks.refresh" }} contextActions={i18n:govoplan-tasks.work} createAction={canWrite ? ( ) : undefined} />
{loading ?

i18n:govoplan-tasks.loading

: null} {!loading && items.length === 0 ?

i18n:govoplan-tasks.empty

: null} {items.length ? ( {items.map((item) => ( setSelectedKey(workKey(item))} className="tasks-list-item" > {item.title} {moduleLabel(item.owner_module)}{dueLabel(item.due_at)} ))} ) : null}
} >
setCreateOpen(false)} onCreated={async (item) => { setCreateOpen(false); setSelectedKey(workKey(item)); await load(); }} onError={setError} setBusy={setBusy} /> setDeferOpen(false)} closeDisabled={busy} portal helpContextId="tasks.action.advance" footer={<>} >
); } function TaskPrimaryActions({ item, busy, onAction }: { item: WorkItem; busy: boolean; onAction: (action: "start" | "complete" | "defer" | "reopen") => void }) { if (["completed", "cancelled"].includes(item.status)) { return ; } return ( <> {["open", "deferred"].includes(item.status) ? : null} ); } function TaskDetails({ item }: { item: WorkItem }) { const safeAction = safeActionUrl(item.action_url); return (
{moduleLabel(item.owner_module)}{item.due_at ? {dueLabel(item.due_at)} : null}

{item.title}

{item.summary ?

{item.summary}

: null} {item.required_action ?
i18n:govoplan-tasks.required_action{item.required_action}
: null} {safeAction ? i18n:govoplan-tasks.open_work : null}

i18n:govoplan-tasks.context

i18n:govoplan-tasks.source_module
{moduleLabel(item.owner_module)}
i18n:govoplan-tasks.provider
{item.provider_id}
i18n:govoplan-tasks.assigned_to
{item.assignments.map(assignmentLabel).join(", ") || "i18n:govoplan-tasks.not_set"}
i18n:govoplan-tasks.updated
{formatDate(item.updated_at)}
{item.sources.length ?

i18n:govoplan-tasks.sources

{item.sources.map((source) =>
{source.label || `${source.resource_type} ${source.resource_id}`}{moduleLabel(source.module_id)}{source.revision ? ` ยท ${source.revision}` : ""}
)}
: null}
); } function CreateTaskDialog({ open, busy, settings, auth, onClose, onCreated, onError, setBusy }: { open: boolean; busy: boolean; settings: ApiSettings; auth: AuthInfo; onClose: () => void; onCreated: (item: WorkItem) => Promise; onError: (message: string) => void; setBusy: (value: boolean) => void }) { const [title, setTitle] = useState(""); const [summary, setSummary] = useState(""); const [requiredAction, setRequiredAction] = useState(""); const [priority, setPriority] = useState("normal"); const [dueAt, setDueAt] = useState(""); const accountId = auth.principal?.account_id || auth.user.account_id; async function submit(event: FormEvent) { event.preventDefault(); if (!title.trim() || !accountId) return; setBusy(true); onError(""); const payload: TaskCreatePayload = { title: title.trim(), summary: summary.trim() || null, required_action: requiredAction.trim() || null, priority, due_at: dueAt ? localDateTimeToIso(dueAt) : null, assignments: [{ kind: "account", id: accountId, label: auth.user.display_name || auth.user.email }], idempotency_key: crypto.randomUUID() }; try { const item = await createTask(settings, payload); setTitle(""); setSummary(""); setRequiredAction(""); setPriority("normal"); setDueAt(""); await onCreated(item); } catch (reason) { onError(errorMessage(reason)); } finally { setBusy(false); } } return ( } >
setTitle(event.target.value)} maxLength={500} autoFocus required />