'use client'; import { useRouter } from 'next/navigation'; import { useState, useTransition } from 'react'; import { cn } from '@/lib/cn'; import type { ReviewDecision } from './types'; /** * Admin mutations. Each button POSTs to a Next route handler (same origin, session cookie) which forwards to FastAPI with the * server-side token, then refreshes the server components. Errors are shown inline — never swallowed. */ async function post(path: string, body?: unknown): Promise<{ ok: true; data: Record } | { ok: false; error: string }> { try { const res = await fetch(path, { method: 'POST', headers: body !== undefined ? { 'content-type': 'application/json' } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined }); const json = (await res.json().catch(() => ({}))) as Record & { error?: { detail?: string; title?: string } }; if (!res.ok) return { ok: false, error: json.error?.detail || json.error?.title || `HTTP ${res.status}` }; return { ok: true, data: json }; } catch (e) { return { ok: false, error: (e as Error).message }; } } const btn = 'inline-flex min-h-9 items-center justify-center rounded-md border px-3 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50'; const btnNeutral = 'border-rule text-ink-2 hover:bg-plane-2 hover:text-ink'; const btnAccent = 'border-accent/40 bg-accent-soft text-accent hover:bg-accent/20'; const btnDanger = 'border-danger/40 bg-danger-soft text-danger hover:bg-danger/20'; export function RunNowButton({ name, className }: { name: string; className?: string }) { const router = useRouter(); const [pending, start] = useTransition(); const [msg, setMsg] = useState<{ tone: 'ok' | 'err'; text: string } | null>(null); return ( {msg && {msg.text}} ); } export function EnabledToggle({ name, enabled, className }: { name: string; enabled: boolean; className?: string }) { const router = useRouter(); const [pending, start] = useTransition(); const [err, setErr] = useState(null); return ( {err && {err}} ); } const DECISION_LABELS: Record = { merged: 'Merge B into A', kept_separate: 'Keep separate', dismissed: 'Dismiss' }; export function ReviewDecisionButtons({ id, canMerge, className }: { id: number; canMerge: boolean; className?: string }) { const router = useRouter(); const [pending, start] = useTransition(); const [msg, setMsg] = useState<{ tone: 'ok' | 'err'; text: string } | null>(null); const [confirmMerge, setConfirmMerge] = useState(false); const decide = (decision: ReviewDecision) => start(async () => { setMsg(null); const r = await post(`/api/admin/review/${id}`, { decision }); if (r.ok) { setMsg({ tone: 'ok', text: `recorded: ${decision.replace('_', ' ')}` }); setConfirmMerge(false); router.refresh(); } else setMsg({ tone: 'err', text: r.error }); }); return (
{canMerge && (confirmMerge ? ( <> ) : ( ))} {pending && Saving…} {msg && {msg.text}}
); }