import { useEffect, useState } from "react"; import { CheckCircle2, Clock, Inbox, Search, SendHorizontal, Undo2, XCircle } from "lucide-react"; import { api, type AccessMessage, type AccessRequest, type AccessStatus } from "../lib/api.ts"; import { Button, CopyButton, Field, Input, Spinner } from "./ui.tsx"; import { cn } from "../lib/cn.ts"; const STATUS_STYLE: Record = { pending: { label: "Pending", cls: "bg-amber-500/10 text-amber-700 dark:text-amber-400" }, reviewing: { label: "Reviewing", cls: "bg-sky-500/10 text-sky-700 dark:text-sky-400" }, accepted: { label: "Accepted", cls: "bg-accent-500/10 text-accent-700 dark:text-accent-400" }, refused: { label: "Refused", cls: "bg-red-500/10 text-red-600 dark:text-red-400" }, }; type Filter = "all" | AccessStatus; const FILTERS: Filter[] = ["all", "pending", "reviewing", "accepted", "refused"]; /** Admin triage for the access-request wishlist: accept / refuse / review. */ export function AccessRequestsPanel() { const [requests, setRequests] = useState<(AccessRequest & { message_count: number })[]>([]); const [filter, setFilter] = useState("pending"); const [selectedId, setSelectedId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); async function refresh(select?: string) { try { const res = await api.adminAccessList(); setRequests(res.requests); if (select) setSelectedId(select); } catch (err) { setError(err instanceof Error ? err.message : "Load failed"); } finally { setLoading(false); } } useEffect(() => { refresh(); }, []); const visible = requests.filter((r) => filter === "all" || r.status === filter); const pendingCount = requests.filter((r) => r.status === "pending").length; return (
{FILTERS.map((f) => { const n = f === "all" ? requests.length : requests.filter((r) => r.status === f).length; return ( ); })}
{error &&

{error}

} {loading &&

Loading…

} {!loading && visible.length === 0 && (

{pendingCount > 0 ? "Nothing with this status." : "No access requests. The wishlist is empty."}

)}
    {visible.map((r) => (
  • {selectedId === r.id && refresh(r.id)} />}
  • ))}
); } function TicketDetail({ request, onChanged }: { request: AccessRequest; onChanged: () => void }) { const [messages, setMessages] = useState([]); const [reason, setReason] = useState(request.reason); const [draft, setDraft] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [freshKey, setFreshKey] = useState(null); useEffect(() => { setReason(request.reason); setFreshKey(null); api.adminAccessGet(request.id).then((res) => setMessages(res.messages)).catch(() => {}); }, [request.id, request.reason]); const closed = request.status === "accepted" || request.status === "refused"; async function setStatus(status: AccessStatus) { setBusy(true); setError(""); try { const res = await api.adminAccessPatch(request.id, { status, reason: reason.trim() }); if (res.key) setFreshKey(res.key); onChanged(); } catch (err) { setError(err instanceof Error ? err.message : "Update failed"); } finally { setBusy(false); } } async function reply(e: React.FormEvent) { e.preventDefault(); const body = draft.trim(); if (!body || busy) return; setBusy(true); setError(""); try { const res = await api.adminAccessReply(request.id, body); setMessages((prev) => [...prev, res.message]); setDraft(""); onChanged(); } catch (err) { setError(err instanceof Error ? err.message : "Send failed"); } finally { setBusy(false); } } return (
{freshKey && (

Account created — key shown once, copy it now:

{freshKey}
)}
{messages.map((m) => (

{m.author === "admin" ? "You (admin)" : `@${request.username}`} · {new Date(m.created_at).toLocaleString()}

{m.body}

))}