import { useCallback, useEffect, useMemo, useState } from "react"; import { CheckCircle2, History, Play, Plus, RefreshCw, UserRoundCog } from "lucide-react"; import { useSearchParams } from "react-router"; import { Button, Card, ConfirmDialog, DateTimeField, Dialog, DismissibleAlert, FormField, PageActionBar, PageLayout, ReferenceSelect, StatusBadge, hasScope } from "@govoplan/core-webui"; import type { ApiSettings, AuthInfo } from "../../types"; import { campaignWorkAssignmentTargetProvider, createCampaignWorkAssignment, listCampaignWorkAssignmentHistory, listCampaignWorkAssignments, reassignCampaignWorkAssignment, reconcileCampaignWorkAssignments, transitionCampaignWorkAssignment, type CampaignWorkAssigneeType, type CampaignWorkAssignment, type CampaignWorkAssignmentEvent } from "../../api/campaigns"; import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData"; type AssignmentDraft = { purpose: string; assigneeType: CampaignWorkAssigneeType; assigneeId: string; dueAt: string; versionId: string; mirrorToTasks: boolean; reason: string; }; const EMPTY_DRAFT: AssignmentDraft = { purpose: "", assigneeType: "account", assigneeId: "", dueAt: "", versionId: "", mirrorToTasks: true, reason: "" }; export default function CampaignWorkPage({ settings, auth, campaignId }: { settings: ApiSettings; auth: AuthInfo; campaignId: string; }) { const workspace = useCampaignWorkspaceData(settings, campaignId); const [searchParams] = useSearchParams(); const requestedAssignmentId = searchParams.get("assignment"); const [assignments, setAssignments] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [hasMore, setHasMore] = useState(false); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [busyId, setBusyId] = useState(""); const [error, setError] = useState(""); const [message, setMessage] = useState(""); const [draft, setDraft] = useState(EMPTY_DRAFT); const [createOpen, setCreateOpen] = useState(false); const [reassigning, setReassigning] = useState(null); const [cancelling, setCancelling] = useState(null); const [rejecting, setRejecting] = useState(null); const [historyFor, setHistoryFor] = useState(null); const [history, setHistory] = useState([]); const [historyCursor, setHistoryCursor] = useState(null); const [historyHasMore, setHistoryHasMore] = useState(false); const [historyLoading, setHistoryLoading] = useState(false); const canManage = hasScope(auth, "campaigns:assignment:manage"); const canComplete = hasScope(auth, "campaigns:assignment:complete"); const targetProvider = useMemo( () => campaignWorkAssignmentTargetProvider(settings, campaignId, draft.assigneeType), [campaignId, draft.assigneeType, settings] ); const loadAssignments = useCallback(async () => { setLoading(true); setError(""); try { const response = await listCampaignWorkAssignments(settings, campaignId); setAssignments(response.items); setNextCursor(response.next_cursor ?? null); setHasMore(response.has_more); } catch (err) { setError(errorText(err)); } finally { setLoading(false); } }, [campaignId, settings]); useEffect(() => { void loadAssignments(); }, [loadAssignments]); useEffect(() => { if (!requestedAssignmentId || loading) return; document .getElementById(`campaign-assignment-${requestedAssignmentId}`) ?.focus({ preventScroll: false }); }, [assignments, loading, requestedAssignmentId]); function replaceAssignment(updated: CampaignWorkAssignment) { setAssignments((current) => current.map((item) => item.id === updated.id ? updated : item)); } async function reload() { setMessage(""); await Promise.all([loadAssignments(), workspace.reload({ force: true })]); } async function loadMore() { if (!nextCursor || loadingMore) return; setLoadingMore(true); setError(""); try { const response = await listCampaignWorkAssignments(settings, campaignId, { cursor: nextCursor }); setAssignments((current) => [ ...current, ...response.items.filter((item) => !current.some((existing) => existing.id === item.id)) ]); setNextCursor(response.next_cursor ?? null); setHasMore(response.has_more); } catch (err) { setError(errorText(err)); } finally { setLoadingMore(false); } } async function createAssignment() { if (!draft.purpose.trim() || !draft.assigneeId || busyId) return; setBusyId("create"); setError(""); try { const created = await createCampaignWorkAssignment(settings, campaignId, { purpose: draft.purpose.trim(), assignee: { type: draft.assigneeType, id: draft.assigneeId }, due_at: draft.dueAt ? new Date(draft.dueAt).toISOString() : null, reference: draft.versionId ? { kind: "campaign_version", id: draft.versionId } : null, mirror_to_tasks: draft.mirrorToTasks }); setAssignments((current) => [created, ...current]); setDraft(EMPTY_DRAFT); setCreateOpen(false); setMessage("Work assignment created. Access and ownership were not changed."); } catch (err) { setError(errorText(err)); } finally { setBusyId(""); } } async function reassign() { if (!reassigning || !draft.assigneeId || busyId) return; setBusyId(reassigning.id); setError(""); try { const updated = await reassignCampaignWorkAssignment(settings, campaignId, reassigning.id, { expected_revision: reassigning.resource_revision, assignee: { type: draft.assigneeType, id: draft.assigneeId }, reason: draft.reason.trim() || null, mirror_to_tasks: draft.mirrorToTasks }); replaceAssignment(updated); setReassigning(null); setDraft(EMPTY_DRAFT); setMessage("Work reassigned; the previous target remains in history."); } catch (err) { setError(errorText(err)); } finally { setBusyId(""); } } async function transition(assignment: CampaignWorkAssignment, action: "accept" | "start" | "complete" | "reject" | "cancel") { if (busyId) return; setBusyId(assignment.id); setError(""); try { const updated = await transitionCampaignWorkAssignment(settings, campaignId, assignment, action); replaceAssignment(updated); setCancelling(null); setRejecting(null); const resultLabel = { accept: "accepted", start: "started", complete: "completed", reject: "rejected", cancel: "cancelled" }[action]; setMessage(`Work ${resultLabel}.`); } catch (err) { setError(errorText(err)); } finally { setBusyId(""); } } async function reconcile() { if (busyId) return; setBusyId("reconcile"); setError(""); try { const result = await reconcileCampaignWorkAssignments(settings, campaignId); await loadAssignments(); setMessage(`Checked ${result.checked} active assignments; ${result.changed} resolution state${result.changed === 1 ? "" : "s"} changed.`); } catch (err) { setError(errorText(err)); } finally { setBusyId(""); } } async function openHistory(assignment: CampaignWorkAssignment) { setHistoryFor(assignment); setHistory([]); setHistoryLoading(true); setError(""); try { const response = await listCampaignWorkAssignmentHistory(settings, campaignId, assignment.id); setHistory(response.items); setHistoryCursor(response.next_cursor ?? null); setHistoryHasMore(response.has_more); } catch (err) { setError(errorText(err)); } finally { setHistoryLoading(false); } } async function loadOlderHistory() { if (!historyFor || !historyCursor || historyLoading) return; setHistoryLoading(true); try { const response = await listCampaignWorkAssignmentHistory(settings, campaignId, historyFor.id, historyCursor); setHistory((current) => [...current, ...response.items]); setHistoryCursor(response.next_cursor ?? null); setHistoryHasMore(response.has_more); } catch (err) { setError(errorText(err)); } finally { setHistoryLoading(false); } } function beginCreate() { setDraft({ ...EMPTY_DRAFT, versionId: workspace.data.campaign?.current_version_id ?? "" }); setCreateOpen(true); } function beginReassign(assignment: CampaignWorkAssignment) { setDraft({ ...EMPTY_DRAFT, assigneeType: assignment.assignee_type, assigneeId: assignment.assignee_id }); setReassigning(assignment); } return ( void reload(), loading: loading || workspace.loading }} createAction={canManage ? : null} contextActions={canManage ? : null} /> } > Assignment records responsibility only. Campaign sharing, ownership transfer, approval and Audit remain separate governed surfaces. {assignments.length === 0 ? (

No work has been assigned for this Campaign.

) : (
    {assignments.map((assignment) => (
  1. {assignment.purpose}

    Assigned to {assignment.assignee_current_label || assignment.assignee_label_snapshot} {` · ${assignment.assignee_type.replace(/_/g, " ")}`}

    Assigned by
    {assignment.assigned_by_label}
    Due
    {assignment.due_at ? formatDate(assignment.due_at) : "No due date"}
    Reference
    {assignment.reference?.label ?? "Campaign"}
    Tasks mirror
    {assignment.task_mirror_status.replace(/_/g, " ")}
    {assignment.assignee_resolution_state !== "resolved" ? ( This target is no longer available or cannot currently be resolved. History is retained; reassign it or restore the separate Campaign access/directory relationship. ) : null}
    {canComplete && assignment.status === "open" ? ( ) : null} {canComplete && (assignment.status === "open" || assignment.status === "in_progress") ? ( ) : null} {canManage && (assignment.status === "open" || assignment.status === "in_progress") ? ( ) : null} {canComplete && (assignment.status === "open" || assignment.status === "in_progress") ? ( ) : null} {canManage && (assignment.status === "open" || assignment.status === "in_progress") ? ( ) : null}
  2. ))}
)} {hasMore ? (
) : null}
!busyId && setCreateOpen(false)} footer={<>} > !busyId && setReassigning(null)} footer={<>} > !historyLoading && setHistoryFor(null)} footer={} > {historyLoading && history.length === 0 ?

Loading history…

: (
    {history.map((event) => (
  1. {event.event_kind.replace(/_/g, " ")}

    {event.assignee_label} · {event.resolution_state.replace(/_/g, " ")}

    {event.actor_label} · {formatDate(event.created_at)}
  2. ))}
)} {historyHasMore ? : null}
setRejecting(null)} onConfirm={() => rejecting ? void transition(rejecting, "reject") : undefined} /> setCancelling(null)} onConfirm={() => cancelling ? void transition(cancelling, "cancel") : undefined} />
); } function AssignmentFields({ draft, setDraft, provider, versions, busy, includePurpose = false, includeReason = false }: { draft: AssignmentDraft; setDraft: (draft: AssignmentDraft) => void; provider: ReturnType; versions: Array<{ id: string; version_number: number }>; busy: boolean; includePurpose?: boolean; includeReason?: boolean; }) { return (
{includePurpose ? (