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/create-mission-form.tsx
6.6 KB
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Plus, X, Loader2, Rocket } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import type { Mission, MissionPriority } from "@/lib/missions/types";
export interface OwnerOption {
id: string;
label: string;
active: boolean;
}
const PRIORITIES: MissionPriority[] = ["P0", "P1", "P2", "P3"];
/**
* Create Mission form. Minimum required fields: Title, Description, Product,
* Repository, Executive Owner, Priority. Status defaults to Planning on the
* server. On success it opens the new mission.
*/
export function CreateMissionForm({
owners,
defaultRepository,
}: {
owners: OwnerOption[];
defaultRepository: string;
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const firstActive = owners.find((o) => o.active)?.id ?? owners[0]?.id ?? "";
const [form, setForm] = useState({
title: "",
description: "",
product: "",
repository: defaultRepository,
executive_owner: firstActive,
priority: "P1" as MissionPriority,
company: "",
});
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
setError(null);
try {
const res = await fetch("/api/missions", {
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 mission.");
const mission = data.mission as Mission;
router.push(`/mission-control/${mission.id}`);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not create mission.");
setBusy(false);
}
}
return (
<div className="flex flex-col items-stretch gap-3 sm:items-end">
<Button type="button" size="sm" onClick={() => setOpen((v) => !v)}>
{open ? <X className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{open ? "Close" : "Create Mission"}
</Button>
{open && (
<Card className="w-full max-w-xl p-5 text-left">
<form className="space-y-4" onSubmit={submit}>
<Field label="Title" required>
<Input
value={form.title}
onChange={(e) => setForm({ ...form, title: e.target.value })}
placeholder="AAF Real Estate Launch"
required
/>
</Field>
<Field label="Description" required>
<textarea
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
rows={3}
required
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
placeholder="What change will this mission make?"
/>
</Field>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Field label="Product" required>
<Input
value={form.product}
onChange={(e) => setForm({ ...form, product: e.target.value })}
placeholder="REOS"
required
/>
</Field>
<Field label="Repository" required>
<Input
value={form.repository}
onChange={(e) => setForm({ ...form, repository: e.target.value })}
className="font-mono text-xs"
required
/>
</Field>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Field label="Executive Owner" required>
<select
value={form.executive_owner}
onChange={(e) => setForm({ ...form, executive_owner: e.target.value })}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{owners.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
{o.active ? "" : " (inactive)"}
</option>
))}
</select>
</Field>
<Field label="Priority" required>
<select
value={form.priority}
onChange={(e) =>
setForm({ ...form, priority: e.target.value as MissionPriority })
}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{PRIORITIES.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</Field>
<Field label="Company">
<Input
value={form.company}
onChange={(e) => setForm({ ...form, company: e.target.value })}
placeholder="optional"
/>
</Field>
</div>
{error && (
<div className="rounded-md bg-destructive/10 px-3 py-2 text-[13px] text-destructive">
{error}
</div>
)}
<div className="flex justify-end">
<Button type="submit" disabled={busy}>
{busy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Rocket className="h-4 w-4" />
)}
Create Mission
</Button>
</div>
</form>
</Card>
)}
</div>
);
}
function Field({
label,
required,
children,
}: {
label: string;
required?: boolean;
children: React.ReactNode;
}) {
return (
<label className="block">
<span className="mb-1.5 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
{label}
{required && <span className="ml-0.5 text-destructive">*</span>}
</span>
{children}
</label>
);
}
root · /srv/aaf