import { Check, CirclePlay, ExternalLink, ListChecks } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link } from "react-router"; import { Button, DismissibleAlert, LoadingFrame, SelectionList, SelectionListItem, SelectionListItemContent, StatusBadge, hasScope, quickAccessLaunchState, useDashboardWidgetData, type QuickAccessToolRenderContext } from "@govoplan/core-webui"; import { listWork, transitionTask, type WorkItem, type WorkStatus } from "../../api/tasks"; const ACTIVE_STATUSES: WorkStatus[] = [ "open", "in_progress", "deferred", "blocked" ]; type Props = Pick< QuickAccessToolRenderContext, "settings" | "auth" | "launchContext" | "complete" >; /** * A bounded projection of the unified inbox. Every load and command goes back * through Tasks, so optional providers retain ownership of visibility and * completion semantics. */ export default function TasksQuickAccess({ settings, auth, launchContext, complete }: Props) { const [refreshKey, setRefreshKey] = useState(0); const [selectedKey, setSelectedKey] = useState(""); const [commandError, setCommandError] = useState(""); const [busy, setBusy] = useState(false); const load = useCallback( () => listWork(settings, { statuses: ACTIVE_STATUSES, limit: 7 }), [settings] ); const { data, loading, error } = useDashboardWidgetData(load, refreshKey); const items = data?.items ?? []; const selected = useMemo( () => items.find((item) => workKey(item) === selectedKey) ?? items[0] ?? null, [items, selectedKey] ); const canWrite = hasScope(auth, "tasks:item:write"); useEffect(() => { if (!selectedKey && items[0]) setSelectedKey(workKey(items[0])); if (selectedKey && !items.some((item) => workKey(item) === selectedKey)) { setSelectedKey(items[0] ? workKey(items[0]) : ""); } }, [items, selectedKey]); async function runCommand(action: "start" | "complete") { if (!selected || selected.provider_id !== "tasks.explicit" || !canWrite) return; setBusy(true); setCommandError(""); try { const updated = await transitionTask(settings, selected, action); if (action === "complete") { complete(workResult(updated, "completed")); return; } setRefreshKey((value) => value + 1); } catch (reason) { setCommandError(errorMessage(reason)); } finally { setBusy(false); } } function selectForHost(item: WorkItem) { complete(workResult(item, "selected")); } const actionPath = selected ? safeActionUrl(selected.action_url) : null; return ( {error ? {error} : null} {commandError ? {commandError} : null} {data?.diagnostics.map((diagnostic) => ( {diagnostic.message} ))} {items.length ? ( {items.map((item) => ( setSelectedKey(workKey(item))} > ))} ) : !loading && !error ? (

i18n:govoplan-tasks.empty

) : null} {selected ? (
{selected.title} {moduleLabel(selected.owner_module)} · {dueLabel(selected.due_at)}
{selected.summary ?

{selected.summary}

: null} {selected.required_action ? (

i18n:govoplan-tasks.required_action: {selected.required_action}

) : null}
{selected.provider_id === "tasks.explicit" && canWrite && ["open", "deferred"].includes(selected.status) ? ( ) : null} {selected.provider_id === "tasks.explicit" && canWrite ? ( ) : null} {actionPath ? ( selectForHost(selected)} >
) : null} {data && data.total > items.length ? (

{items.length} / {data.total} · i18n:govoplan-tasks.open_work_inbox

) : null}
); } function workResult(item: WorkItem, action: "selected" | "completed") { return { contractVersion: "1" as const, outcome: "completed" as const, action, reference: { ownerModule: "tasks", kind: "work-item", objectId: `${item.provider_id}:${item.id}`, tenantId: item.tenant_id, label: item.title, version: item.revision, path: safeActionUrl(item.action_url) || "/tasks" } }; } function workKey(item: WorkItem): string { return `${item.provider_id}:${item.id}`; } function safeActionUrl(value?: string | null): string | null { if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return null; return value; } function statusLabel(value: string): string { return `i18n:govoplan-tasks.status.${value}`; } function moduleLabel(value: string): string { return value.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } function dueLabel(value?: string | null): string { if (!value) return "i18n:govoplan-tasks.no_due_date"; const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) return value; return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(parsed); } function errorMessage(reason: unknown): string { return reason instanceof Error ? reason.message : "i18n:govoplan-tasks.request_failed"; }