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/objective-edit.tsx
5.8 KB
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Pencil, X, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import type { OwnerOption } from "./create-mission-form";
import type { MissionPriority, Objective } from "@/lib/missions/types";

const PRIORITIES: MissionPriority[] = ["P0", "P1", "P2", "P3"];

/** Edit an objective's fields (not its status — that is a state_changed action). */
export function ObjectiveEdit({
  objective,
  owners,
}: {
  objective: Objective;
  owners: OwnerOption[];
}) {
  const router = useRouter();
  const [open, setOpen] = useState(false);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [form, setForm] = useState({
    title: objective.title,
    description: objective.description,
    owner_executive: objective.owner_executive ?? "",
    priority: objective.priority,
    success_criteria: objective.success_criteria.join("\n"),
    completion_percentage: objective.completion_percentage,
  });

  async function save(e: React.FormEvent) {
    e.preventDefault();
    setBusy(true);
    setError(null);
    try {
      const res = await fetch(
        `/api/missions/${objective.mission_id}/objectives/${objective.id}`,
        {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            ...form,
            success_criteria: form.success_criteria
              .split("\n")
              .map((s) => s.trim())
              .filter(Boolean),
            completion_percentage: Number(form.completion_percentage),
          }),
        },
      );
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error ?? "Update failed.");
      setOpen(false);
      router.refresh();
    } catch (err) {
      setError(err instanceof Error ? err.message : "Update failed.");
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="flex flex-col items-end gap-2">
      <Button type="button" variant="outline" size="sm" onClick={() => setOpen((v) => !v)}>
        {open ? <X className="h-3.5 w-3.5" /> : <Pencil className="h-3.5 w-3.5" />}
        {open ? "Close" : "Edit"}
      </Button>
      {error && <span className="text-[11px] text-destructive">{error}</span>}

      {open && (
        <Card className="w-full max-w-md p-5 text-left">
          <form className="space-y-3" onSubmit={save}>
            <Input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
            <textarea
              value={form.description}
              onChange={(e) => setForm({ ...form, description: e.target.value })}
              rows={2}
              className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            />
            <label className="block">
              <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
                Success criteria (one per line)
              </span>
              <textarea
                value={form.success_criteria}
                onChange={(e) => setForm({ ...form, success_criteria: e.target.value })}
                rows={3}
                className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
              />
            </label>
            <div className="grid grid-cols-3 gap-3">
              <label>
                <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
                  Owner
                </span>
                <select
                  value={form.owner_executive}
                  onChange={(e) => setForm({ ...form, owner_executive: e.target.value })}
                  className="h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
                >
                  {owners.map((o) => (
                    <option key={o.id} value={o.id}>
                      {o.label}
                    </option>
                  ))}
                </select>
              </label>
              <label>
                <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
                  Priority
                </span>
                <select
                  value={form.priority}
                  onChange={(e) => setForm({ ...form, priority: e.target.value as MissionPriority })}
                  className="h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
                >
                  {PRIORITIES.map((p) => (
                    <option key={p}>{p}</option>
                  ))}
                </select>
              </label>
              <label>
                <span className="mb-1 block text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
                  Progress %
                </span>
                <Input
                  type="number"
                  min={0}
                  max={100}
                  value={form.completion_percentage}
                  onChange={(e) =>
                    setForm({ ...form, completion_percentage: Number(e.target.value) })
                  }
                  className="h-9"
                />
              </label>
            </div>
            <div className="flex justify-end">
              <Button type="submit" size="sm" disabled={busy}>
                {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Save"}
              </Button>
            </div>
          </form>
        </Card>
      )}
    </div>
  );
}

root · /srv/aaf