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/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