import { ArrowLeft, CheckCircle2, RefreshCw, ShieldCheck } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { api, errorMessage } from "../api"; import { Alert, JsonBlock, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui"; import { formatDate } from "../lib/format"; import type { Navigate } from "../routing"; import type { ModerationCaseDetail, ModerationReport } from "../types"; import { CaseSeverity, CaseStatus, moderationEnumLabel, moderationTargetLabel } from "./ModerationCasesPage"; type DecisionPreset = "no_violation" | "scam" | "fake" | "freeze" | "scam_freeze" | "fake_freeze" | "delete_messages" | "delete_account"; export function ModerationCaseDetailPage({ id, navigate }: { id: number; navigate: Navigate }) { const [detail, setDetail] = useState(null); const [report, setReport] = useState(null); const [reason, setReason] = useState(""); const [preset, setPreset] = useState("no_violation"); const [messageIDs, setMessageIDs] = useState(""); const [ownerUserID, setOwnerUserID] = useState(""); const [revokeMessages, setRevokeMessages] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); function selectReport(next: ModerationReport | null) { setReport(next); if (!next) return; const ids = next.Items .filter((item) => item.Kind === "message") .map((item) => Number(item.ItemID)) .filter((value) => Number.isSafeInteger(value) && value > 0); setMessageIDs(ids.join(", ")); setOwnerUserID(String(next.ReporterUserID)); } async function load() { setError(""); try { const next = await api.moderationCase(id); setDetail(next); const reportID = next.ReportIDs[0]; selectReport(reportID ? await api.moderationReport(reportID) : null); } catch (err) { setError(errorMessage(err)); } } useEffect(() => { void load(); }, [id]); const selectedActions = useMemo( () => actionsForPreset( preset, detail?.Case.Target.Type, parseMessageIDs(messageIDs), Number(ownerUserID), revokeMessages ), [preset, detail?.Case.Target.Type, messageIDs, ownerUserID, revokeMessages] ); const appealRemedy = useMemo( () => detail ? requiredAppealRemedy(detail) : { actions: [], label: "None", blocked: false }, [detail] ); async function claim() { if (!detail) return; setBusy(true); setError(""); try { await api.claimModerationCase(id, detail.Case.Version); await load(); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } async function decide() { if (!detail || !reason.trim()) { setError("A review reason is required."); return; } if (preset === "delete_messages" && selectedActions.length === 0) { setError(detail.Case.Target.Type === "user" ? "Private-message deletion requires valid evidence message IDs and the reporter's owner_user_id." : "Channel-message deletion requires at least one valid evidence message ID."); return; } if (!window.confirm(`Submit the “${decisionPresetLabel(preset)}” decision? The action will run through the durable action queue.`)) return; setBusy(true); setError(""); try { const result = await api.decideModerationCase(id, { expected_version: detail.Case.Version, reason: reason.trim(), kind: preset === "no_violation" ? "no_violation" : "violation", actions: selectedActions }); setDetail(result.case); setReason(""); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } async function reviewAppeal(appealID: number, granted: boolean) { if (!detail || !reason.trim()) { setError("An appeal review reason is required."); return; } if (!window.confirm(granted ? "Grant this appeal?" : "Deny this appeal?")) return; setBusy(true); try { const result = await api.reviewModerationAppeal(id, appealID, { expected_version: detail.Case.Version, reason: reason.trim(), granted, actions: granted ? appealRemedy.actions : [] }); setDetail(result.case); setReason(""); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } if (error && !detail) return {error}; if (!detail) return ; const item = detail.Case; const canClaim = item.Status === "open" || item.Status === "in_review" || item.Status === "appeal_review"; const canDecide = (item.Status === "in_review" || item.Status === "action_failed") && Boolean(item.AssignedTo); const canSubmitDecision = canDecide && (item.Status !== "action_failed" || preset !== "no_violation"); const pendingAppeal = detail.Appeals.find((appeal) => appeal.Status === "pending"); return ( } > {error && {error}}
{moderationTargetLabel(item.Target.Type, item.Target.ID)}
{`Version ${item.Version} · Updated ${formatDate(item.UpdatedAt)}`}
{detail.ReportIDs.map((reportID) => ( ))}
{report && ( <>
{report.Comment &&

{report.Comment}

} )}
{detail.Appeals.length > 0 && (
)} } side={
{"Case actions"}
{canClaim && ( )}