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/mission-state-actions.tsx
2.4 KB
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Loader2, AlertTriangle, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { allowedNextStates, TRANSITION_LABEL } from "@/lib/missions/lifecycle";
import type { MissionStatus } from "@/lib/missions/types";
/**
* Governed state-change actions: one button per *valid* next state. Invalid
* transitions are never offered (and are rejected server-side regardless).
*/
export function MissionStateActions({
missionId,
status,
}: {
missionId: string;
status: MissionStatus;
}) {
const router = useRouter();
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const next = allowedNextStates(status);
async function transition(to: MissionStatus) {
setBusy(to);
setError(null);
try {
const res = await fetch(`/api/missions/${missionId}/transition`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ to }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error ?? "Transition failed.");
router.refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Transition failed.");
} finally {
setBusy(null);
}
}
if (next.length === 0) {
return (
<p className="text-[12px] text-muted-foreground">
Terminal state — no further transitions.
</p>
);
}
return (
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
{next.map((to) => (
<Button
key={to}
type="button"
variant="outline"
size="sm"
onClick={() => transition(to)}
disabled={busy !== null}
>
{busy === to ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<ArrowRight className="h-3.5 w-3.5" />
)}
{TRANSITION_LABEL[to]}
</Button>
))}
</div>
{error && (
<div className="flex items-start gap-1.5 text-[12px] text-destructive">
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span className="whitespace-pre-line">{error}</span>
</div>
)}
</div>
);
}
root · /srv/aaf