Intelligence
Artifacts
Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.
Repository
assignment-dispatch-button.tsxassignment-status-badge.tsxassignments-panel.tsxcreate-mission-form.tsxgovernance-panels.tsxmission-dispatch.tsxmission-edit.tsxmission-state-actions.tsxmission-status-badge.tsxobjective-edit.tsxobjective-status-badge.tsxobjective-status-select.tsxobjectives-panel.tsxreport-list.tsxwork-orders-panel.tsx
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