Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
.gitignoreDockerfilenext-env.d.tsnext.config.mjspackage-lock.jsonpackage.jsonpostcss.config.mjsREADME.mdtailwind.config.tstsconfig.jsontsconfig.tsbuildinfo
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/components/missions/assignments-panel.tsx
7.7 KB
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Plus, X, Loader2, GitBranch, Send, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { AssignmentStatusBadge } from "./assignment-status-badge";
import type { OwnerOption } from "./create-mission-form";
import type { Assignment, WorkOrder } from "@/lib/missions/types";

/**
 * Assignments panel: delegate a work order and dispatch it. Creating an
 * assignment auto-generates its execution context. Dispatch is assignment-based.
 */
export function AssignmentsPanel({
  missionId,
  assignments,
  workOrders,
  owners,
}: {
  missionId: string;
  assignments: Assignment[];
  workOrders: WorkOrder[];
  owners: OwnerOption[];
}) {
  const router = useRouter();
  const [open, setOpen] = useState(false);
  const [busy, setBusy] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const firstActive = owners.find((o) => o.active)?.id ?? owners[0]?.id ?? "";
  const [form, setForm] = useState({
    work_order_id: workOrders[0]?.id ?? "",
    executive: firstActive,
    repository: "",
  });

  async function create(e: React.FormEvent) {
    e.preventDefault();
    setBusy("create");
    setError(null);
    try {
      const res = await fetch(`/api/missions/${missionId}/assignments`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(form),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error ?? "Could not create assignment.");
      setOpen(false);
      router.refresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : "Could not create assignment.");
    } finally {
      setBusy(null);
    }
  }

  async function dispatch(aid: string) {
    setBusy(aid);
    setError(null);
    try {
      const res = await fetch(
        `/api/missions/${missionId}/assignments/${aid}/dispatch`,
        { method: "POST" },
      );
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error ?? "Dispatch failed.");
      router.refresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : "Dispatch failed.");
    } finally {
      setBusy(null);
    }
  }

  const woTitle = (wid: string) =>
    workOrders.find((w) => w.id === wid)?.title ?? wid;

  return (
    <Card className="p-6">
      <div className="mb-4 flex items-center justify-between gap-3">
        <div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.1em] text-muted-foreground">
          <GitBranch className="h-3.5 w-3.5" /> Assignments
        </div>
        <Button
          type="button"
          size="sm"
          variant="outline"
          onClick={() => setOpen((v) => !v)}
          disabled={workOrders.length === 0}
          title={workOrders.length === 0 ? "Create a work order first" : ""}
        >
          {open ? <X className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
          {open ? "Close" : "Create Assignment"}
        </Button>
      </div>

      {open && (
        <form onSubmit={create} className="mb-5 space-y-3 rounded-md border border-border p-4">
          <label className="block">
            <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
              Work Order
            </span>
            <select
              value={form.work_order_id}
              onChange={(e) => setForm({ ...form, work_order_id: e.target.value })}
              className="h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
            >
              {workOrders.map((w) => (
                <option key={w.id} value={w.id}>
                  {w.id.replace("WORKORDER-", "WO-")} · {w.title}
                </option>
              ))}
            </select>
          </label>
          <div className="flex flex-wrap items-end gap-3">
            <label className="flex-1">
              <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
                Executive
              </span>
              <select
                value={form.executive}
                onChange={(e) => setForm({ ...form, executive: e.target.value })}
                className="h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
              >
                {owners.map((o) => (
                  <option key={o.id} value={o.id}>
                    {o.label}
                    {o.active ? "" : " (inactive)"}
                  </option>
                ))}
              </select>
            </label>
            <label className="flex-1">
              <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
                Repository (optional)
              </span>
              <Input
                value={form.repository}
                onChange={(e) => setForm({ ...form, repository: e.target.value })}
                placeholder="mission default"
                className="h-9 font-mono text-xs"
              />
            </label>
            <Button type="submit" size="sm" disabled={busy !== null}>
              {busy === "create" ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Create"}
            </Button>
          </div>
        </form>
      )}

      {error && <p className="mb-3 text-[12px] text-destructive">{error}</p>}

      {assignments.length === 0 ? (
        <p className="text-sm text-muted-foreground">
          No assignments yet. An assignment delegates one work order and generates
          its execution context.
        </p>
      ) : (
        <ul className="divide-y divide-border">
          {assignments.map((a) => (
            <li key={a.id} className="flex flex-wrap items-center gap-3 py-2.5">
              <div className="min-w-0 flex-1">
                <Link
                  href={`/mission-control/${missionId}/assignments/${a.id}`}
                  className="inline-flex items-center gap-1.5 text-[14px] font-medium text-foreground hover:underline"
                >
                  <span className="font-mono text-[11px] text-muted-foreground">
                    {a.id.replace("ASSIGNMENT-", "A-")}
                  </span>
                  {woTitle(a.work_order_id)}
                  <ArrowRight className="h-3 w-3 text-muted-foreground/50" />
                </Link>
                <div className="mt-0.5 text-[12px] text-muted-foreground">
                  <span className="font-mono">{a.executive}</span> · {a.work_order_id.replace("WORKORDER-", "WO-")}
                  {a.session && (
                    <>
                      {" · "}
                      <Link
                        href={`/sessions/${a.session.session_id}`}
                        className="underline underline-offset-2"
                      >
                        session
                      </Link>
                    </>
                  )}
                </div>
              </div>
              <AssignmentStatusBadge status={a.status} />
              {(a.status === "Pending" || a.status === "Failed") && (
                <Link
                  href={`/mission-control/${missionId}/assignments/${a.id}`}
                  className="inline-flex items-center gap-1 rounded-md border border-input px-2.5 py-1 text-[12px] font-medium hover:bg-secondary"
                >
                  <Send className="h-3.5 w-3.5" /> Instantiate
                </Link>
              )}
            </li>
          ))}
        </ul>
      )}
    </Card>
  );
}

root · /srv/aaf