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/governance-panels.tsx
18.9 KB
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import {
Plus, X, Loader2, ChevronDown, ChevronRight,
ShieldAlert, Gavel, Link2, Gauge,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import type { OwnerOption } from "./create-mission-form";
import type {
Decision, Dependency, DependencyStatus, DependencyType,
GovernanceEvent, Kpi, KpiStatus, Risk, RiskLikelihood, RiskSeverity, RiskStatus,
WithHistory,
} from "@/lib/missions/types";
// --- shared helpers --------------------------------------------------------
async function send(url: string, method: string, body: unknown): Promise<string | null> {
const res = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
return d.error ?? `Request failed (${res.status}).`;
}
return null;
}
function HistoryList({ history }: { history: GovernanceEvent[] }) {
if (history.length === 0) return null;
return (
<ol className="mt-2 space-y-1 border-l border-border pl-3">
{history.map((e, i) => (
<li key={i} className="text-[11px] text-muted-foreground">
<span className="font-medium text-foreground/70">{e.type.replace("_", " ")}</span>
{e.previous && e.next ? ` (${e.previous} → ${e.next})` : e.detail ? ` — ${e.detail}` : ""}
{" · "}
{new Date(e.at).toLocaleString()}
</li>
))}
</ol>
);
}
function PanelShell({
title, icon: Icon, count, open, setOpen, disabled, children, form,
}: {
title: string; icon: typeof ShieldAlert; count: number;
open: boolean; setOpen: (v: boolean) => void; disabled?: boolean;
children: React.ReactNode; form: React.ReactNode;
}) {
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">
<Icon className="h-3.5 w-3.5" /> {title} ({count})
</div>
<Button type="button" size="sm" variant="outline" onClick={() => setOpen(!open)} disabled={disabled}>
{open ? <X className="h-3.5 w-3.5" /> : <Plus className="h-3.5 w-3.5" />}
{open ? "Close" : "Create"}
</Button>
</div>
{open && <div className="mb-4 rounded-md border border-border p-4">{form}</div>}
{children}
</Card>
);
}
const inputCls =
"flex h-9 w-full rounded-md border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
function Row({
summary, meta, control, children,
}: {
summary: React.ReactNode; meta?: React.ReactNode; control?: React.ReactNode; children?: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
return (
<li className="py-2.5">
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={() => setOpen((v) => !v)} className="text-muted-foreground">
{open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
<div className="min-w-0 flex-1">{summary}{meta && <div className="text-[12px] text-muted-foreground">{meta}</div>}</div>
{control}
</div>
{open && children && <div className="mt-2 pl-6">{children}</div>}
</li>
);
}
// --- Risks -----------------------------------------------------------------
const SEVERITY: RiskSeverity[] = ["Low", "Medium", "High", "Critical"];
const LIKELIHOOD: RiskLikelihood[] = ["Low", "Medium", "High"];
const RISK_STATUS: RiskStatus[] = ["Open", "Mitigating", "Resolved", "Accepted", "Closed"];
const SEV_STYLE: Record<RiskSeverity, string> = {
Low: "bg-slate-100 text-slate-600", Medium: "bg-blue-50 text-blue-700",
High: "bg-amber-50 text-amber-700", Critical: "bg-red-50 text-red-700",
};
export function RisksPanel({ missionId, risks, owners }: {
missionId: string; risks: WithHistory<Risk>[]; owners: OwnerOption[];
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [f, setF] = useState({ title: "", description: "", severity: "Medium" as RiskSeverity, likelihood: "Medium" as RiskLikelihood, owner_executive: owners[0]?.id ?? "", mitigation: "" });
async function create(e: React.FormEvent) {
e.preventDefault(); setBusy(true); setError(null);
const err = await send(`/api/missions/${missionId}/risks`, "POST", f);
setBusy(false);
if (err) return setError(err);
setF({ ...f, title: "", description: "", mitigation: "" }); setOpen(false); router.refresh();
}
async function patch(rid: string, body: unknown) {
const err = await send(`/api/missions/${missionId}/risks/${rid}`, "PATCH", body);
if (!err) router.refresh();
}
return (
<PanelShell title="Risks" icon={ShieldAlert} count={risks.length} open={open} setOpen={setOpen}
form={
<form onSubmit={create} className="space-y-2">
<Input value={f.title} onChange={(e) => setF({ ...f, title: e.target.value })} placeholder="Risk title" required />
<textarea value={f.description} onChange={(e) => setF({ ...f, description: e.target.value })} placeholder="Description" rows={2} className={inputCls + " h-auto py-2"} />
<div className="flex flex-wrap gap-2">
<select value={f.severity} onChange={(e) => setF({ ...f, severity: e.target.value as RiskSeverity })} className={inputCls + " w-auto"}>{SEVERITY.map((s) => <option key={s}>{s}</option>)}</select>
<select value={f.likelihood} onChange={(e) => setF({ ...f, likelihood: e.target.value as RiskLikelihood })} className={inputCls + " w-auto"}>{LIKELIHOOD.map((s) => <option key={s}>{s}</option>)}</select>
<select value={f.owner_executive} onChange={(e) => setF({ ...f, owner_executive: e.target.value })} className={inputCls + " w-auto"}>{owners.map((o) => <option key={o.id} value={o.id}>{o.id}</option>)}</select>
<Button type="submit" size="sm" disabled={busy}>{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Add Risk"}</Button>
</div>
{error && <p className="text-[12px] text-destructive">{error}</p>}
</form>
}>
{risks.length === 0 ? <p className="text-sm text-muted-foreground">No risks recorded.</p> : (
<ul className="divide-y divide-border">
{risks.map(({ item: r, history }) => (
<Row key={r.id}
summary={<div className="flex items-center gap-2 text-[14px] font-medium text-foreground"><span className="font-mono text-[11px] text-muted-foreground">{r.id.replace("RISK-", "R-")}</span>{r.title}<span className={cn("rounded-full px-2 py-0.5 text-[10px] font-medium", SEV_STYLE[r.severity])}>{r.severity}</span></div>}
meta={`${r.likelihood} likelihood · ${r.owner_executive ?? "—"}`}
control={<select value={r.status} onChange={(e) => patch(r.id, { status: e.target.value })} className="h-7 rounded-md border border-input bg-background px-2 text-[12px]">{RISK_STATUS.map((s) => <option key={s}>{s}</option>)}</select>}
>
{r.description && <p className="text-[13px] text-foreground/90">{r.description}</p>}
<div className="mt-2">
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Mitigation</span>
<Input defaultValue={r.mitigation} onBlur={(e) => e.target.value !== r.mitigation && patch(r.id, { mitigation: e.target.value })} placeholder="Mitigation (blur to save)" className="mt-1 h-8" />
</div>
<HistoryList history={history} />
</Row>
))}
</ul>
)}
</PanelShell>
);
}
// --- Decisions (immutable) -------------------------------------------------
export function DecisionsPanel({ missionId, decisions, owners }: {
missionId: string; decisions: WithHistory<Decision>[]; owners: OwnerOption[];
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [f, setF] = useState({ title: "", description: "", decision: "", rationale: "", alternatives_considered: "", approved_by: owners[0]?.id ?? "CEO" });
async function create(e: React.FormEvent) {
e.preventDefault(); setBusy(true); setError(null);
const err = await send(`/api/missions/${missionId}/decisions`, "POST", {
...f, alternatives_considered: f.alternatives_considered.split("\n").map((s) => s.trim()).filter(Boolean),
});
setBusy(false);
if (err) return setError(err);
setF({ ...f, title: "", description: "", decision: "", rationale: "", alternatives_considered: "" }); setOpen(false); router.refresh();
}
return (
<PanelShell title="Decisions" icon={Gavel} count={decisions.length} open={open} setOpen={setOpen}
form={
<form onSubmit={create} className="space-y-2">
<Input value={f.title} onChange={(e) => setF({ ...f, title: e.target.value })} placeholder="Decision title" required />
<Input value={f.decision} onChange={(e) => setF({ ...f, decision: e.target.value })} placeholder="The decision made" required />
<textarea value={f.rationale} onChange={(e) => setF({ ...f, rationale: e.target.value })} placeholder="Rationale" rows={2} className={inputCls + " h-auto py-2"} />
<textarea value={f.alternatives_considered} onChange={(e) => setF({ ...f, alternatives_considered: e.target.value })} placeholder="Alternatives considered (one per line)" rows={2} className={inputCls + " h-auto py-2"} />
<div className="flex gap-2">
<Input value={f.approved_by} onChange={(e) => setF({ ...f, approved_by: e.target.value })} placeholder="Approved by" className="flex-1" required />
<Button type="submit" size="sm" disabled={busy}>{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Record"}</Button>
</div>
<p className="text-[11px] text-muted-foreground">Decisions are immutable. Corrections create a new decision.</p>
{error && <p className="text-[12px] text-destructive">{error}</p>}
</form>
}>
{decisions.length === 0 ? <p className="text-sm text-muted-foreground">No decisions recorded.</p> : (
<ul className="divide-y divide-border">
{decisions.map(({ item: d, history }) => (
<Row key={d.id}
summary={<div className="flex items-center gap-2 text-[14px] font-medium text-foreground"><span className="font-mono text-[11px] text-muted-foreground">{d.id.replace("DECISION-", "D-")}</span>{d.title}</div>}
meta={`Decided: ${d.decision} · by ${d.approved_by}`}
>
{d.rationale && <p className="text-[13px] text-foreground/90"><span className="font-medium">Rationale:</span> {d.rationale}</p>}
{d.alternatives_considered.length > 0 && <p className="mt-1 text-[12px] text-muted-foreground">Alternatives: {d.alternatives_considered.join("; ")}</p>}
<HistoryList history={history} />
</Row>
))}
</ul>
)}
</PanelShell>
);
}
// --- Dependencies ----------------------------------------------------------
const DEP_TYPE: DependencyType[] = ["Mission", "Objective", "WorkOrder", "Assignment", "Asset", "External"];
const DEP_STATUS: DependencyStatus[] = ["Pending", "Active", "Resolved", "Cancelled"];
export function DependenciesPanel({ missionId, dependencies }: {
missionId: string; dependencies: WithHistory<Dependency>[];
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [f, setF] = useState({ depends_on: "", type: "External" as DependencyType, blocking: true, description: "" });
async function create(e: React.FormEvent) {
e.preventDefault(); setBusy(true); setError(null);
const err = await send(`/api/missions/${missionId}/dependencies`, "POST", f);
setBusy(false);
if (err) return setError(err);
setF({ ...f, depends_on: "", description: "" }); setOpen(false); router.refresh();
}
async function patch(did: string, body: unknown) {
const err = await send(`/api/missions/${missionId}/dependencies/${did}`, "PATCH", body);
if (!err) router.refresh();
}
return (
<PanelShell title="Dependencies" icon={Link2} count={dependencies.length} open={open} setOpen={setOpen}
form={
<form onSubmit={create} className="space-y-2">
<div className="flex flex-wrap gap-2">
<select value={f.type} onChange={(e) => setF({ ...f, type: e.target.value as DependencyType })} className={inputCls + " w-auto"}>{DEP_TYPE.map((t) => <option key={t}>{t}</option>)}</select>
<Input value={f.depends_on} onChange={(e) => setF({ ...f, depends_on: e.target.value })} placeholder="Depends on (id or label)" className="flex-1" required />
</div>
<textarea value={f.description} onChange={(e) => setF({ ...f, description: e.target.value })} placeholder="Description" rows={2} className={inputCls + " h-auto py-2"} />
<label className="flex items-center gap-2 text-[13px]">
<input type="checkbox" checked={f.blocking} onChange={(e) => setF({ ...f, blocking: e.target.checked })} /> Blocking
</label>
<Button type="submit" size="sm" disabled={busy}>{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Add Dependency"}</Button>
{error && <p className="text-[12px] text-destructive">{error}</p>}
</form>
}>
{dependencies.length === 0 ? <p className="text-sm text-muted-foreground">No dependencies recorded.</p> : (
<ul className="divide-y divide-border">
{dependencies.map(({ item: d, history }) => (
<Row key={d.id}
summary={<div className="flex items-center gap-2 text-[14px] font-medium text-foreground"><span className="font-mono text-[11px] text-muted-foreground">{d.id.replace("DEPENDENCY-", "DEP-")}</span>{d.depends_on}<span className="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-foreground/70">{d.type}</span>{d.blocking && <span className="rounded-full bg-red-50 px-2 py-0.5 text-[10px] font-medium text-red-700">blocking</span>}</div>}
control={<select value={d.status} onChange={(e) => patch(d.id, { status: e.target.value })} className="h-7 rounded-md border border-input bg-background px-2 text-[12px]">{DEP_STATUS.map((s) => <option key={s}>{s}</option>)}</select>}
>
{d.description && <p className="text-[13px] text-foreground/90">{d.description}</p>}
<label className="mt-2 flex items-center gap-2 text-[12px]">
<input type="checkbox" checked={d.blocking} onChange={(e) => patch(d.id, { blocking: e.target.checked })} /> Blocking
</label>
<HistoryList history={history} />
</Row>
))}
</ul>
)}
</PanelShell>
);
}
// --- KPIs ------------------------------------------------------------------
const KPI_STATUS: KpiStatus[] = ["On Track", "At Risk", "Off Track", "Achieved"];
export function KpisPanel({ missionId, kpis }: {
missionId: string; kpis: WithHistory<Kpi>[];
}) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [f, setF] = useState({ name: "", description: "", current_value: "", target_value: "", unit: "", status: "On Track" as KpiStatus });
async function create(e: React.FormEvent) {
e.preventDefault(); setBusy(true); setError(null);
const err = await send(`/api/missions/${missionId}/kpis`, "POST", f);
setBusy(false);
if (err) return setError(err);
setF({ ...f, name: "", description: "", current_value: "", target_value: "", unit: "" }); setOpen(false); router.refresh();
}
async function patch(kid: string, body: unknown) {
const err = await send(`/api/missions/${missionId}/kpis/${kid}`, "PATCH", body);
if (!err) router.refresh();
}
return (
<PanelShell title="KPIs" icon={Gauge} count={kpis.length} open={open} setOpen={setOpen}
form={
<form onSubmit={create} className="space-y-2">
<Input value={f.name} onChange={(e) => setF({ ...f, name: e.target.value })} placeholder="KPI name" required />
<div className="flex flex-wrap gap-2">
<Input value={f.current_value} onChange={(e) => setF({ ...f, current_value: e.target.value })} placeholder="Current" className="w-24" />
<Input value={f.target_value} onChange={(e) => setF({ ...f, target_value: e.target.value })} placeholder="Target" className="w-24" />
<Input value={f.unit} onChange={(e) => setF({ ...f, unit: e.target.value })} placeholder="Unit" className="w-24" />
<select value={f.status} onChange={(e) => setF({ ...f, status: e.target.value as KpiStatus })} className={inputCls + " w-auto"}>{KPI_STATUS.map((s) => <option key={s}>{s}</option>)}</select>
<Button type="submit" size="sm" disabled={busy}>{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Add KPI"}</Button>
</div>
<p className="text-[11px] text-muted-foreground">KPIs are informational — no automation.</p>
{error && <p className="text-[12px] text-destructive">{error}</p>}
</form>
}>
{kpis.length === 0 ? <p className="text-sm text-muted-foreground">No KPIs recorded.</p> : (
<ul className="divide-y divide-border">
{kpis.map(({ item: k, history }) => (
<Row key={k.id}
summary={<div className="flex items-center gap-2 text-[14px] font-medium text-foreground"><span className="font-mono text-[11px] text-muted-foreground">{k.id.replace("KPI-", "K-")}</span>{k.name}</div>}
meta={`${k.current_value || "—"} / ${k.target_value || "—"} ${k.unit}`}
control={<select value={k.status} onChange={(e) => patch(k.id, { status: e.target.value })} className="h-7 rounded-md border border-input bg-background px-2 text-[12px]">{KPI_STATUS.map((s) => <option key={s}>{s}</option>)}</select>}
>
{k.description && <p className="text-[13px] text-foreground/90">{k.description}</p>}
<div className="mt-2 flex items-center gap-2">
<span className="text-[11px] uppercase tracking-[0.08em] text-muted-foreground">Current</span>
<Input defaultValue={k.current_value} onBlur={(e) => e.target.value !== k.current_value && patch(k.id, { current_value: e.target.value })} className="h-8 w-32" />
</div>
<HistoryList history={history} />
</Row>
))}
</ul>
)}
</PanelShell>
);
}
root · /srv/aaf