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