"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { UserMinusIcon } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cancelDivisionRemoval } from "@/app/(frontend)/roster/actions"; import { LeadershipTransferCard } from "@/components/frontend/transfers/LeadershipTransferCard"; import { formatTransferDate } from "@/components/frontend/transfers/transferTypes"; import type { Assignment, LeadershipTransfer, User } from "@/payload-types"; interface LeadershipTransfersSectionProps { readonly currentUserId: number; readonly canResolve: boolean; } const LEADERSHIP_QUERY = new URLSearchParams({ depth: "1", limit: "50", sort: "-createdAt", }); const STATUS_LABELS: Record = { pending: "Pending approval", approved: "Approved", denied: "Denied", cancelled: "Cancelled", }; const STATUS_VARIANTS: Record< LeadershipTransfer["status"], "default" | "secondary" | "destructive" | "outline" > = { pending: "secondary", approved: "default", denied: "destructive", cancelled: "outline", }; function initiatorId(value: number | User | null | undefined): number | null { if (value == null) return null; return typeof value === "number" ? value : value.id; } function personRefLabel(value: number | User | null | undefined, fallback = "Unknown member"): string { if (value == null) return fallback; if (typeof value === "number") return `User #${value}`; return value.displayName || value.username || fallback; } function assignmentRef(value: number | Assignment | null | undefined): string { if (value == null) return "Unknown assignment"; if (typeof value === "number") return `Assignment #${value}`; return value.callsign ? `[${value.callsign}] ${value.name}` : value.name; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } /** Shallow-shape-check the REST page so a malformed response surfaces as an error. */ function parseDocs(data: unknown): LeadershipTransfer[] | null { if (!isRecord(data) || !Array.isArray(data.docs)) return null; return data.docs.filter((doc) => isRecord(doc) && typeof doc.id === "number") as LeadershipTransfer[]; } /** * Leadership-initiated removal requests on the transfers page. * * Superusers get the pending queue (Approve/Deny with note) plus a collapsed * history. Regular members only ever see their own requests as status chips; * the whole section stays hidden when they have none (pending removals are * invisible to the unit by design). */ export function LeadershipTransfersSection({ currentUserId, canResolve, }: LeadershipTransfersSectionProps) { const [docs, setDocs] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [cancellingId, setCancellingId] = useState(null); const [historyOpen, setHistoryOpen] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch(`/api/leadership-transfers?${LEADERSHIP_QUERY.toString()}`, { cache: "no-store", }); if (!response.ok) throw new Error("Could not load leadership transfers."); const body: unknown = await response.json(); const parsed = parseDocs(body); if (!parsed) throw new Error("The leadership transfer response was invalid."); setDocs(parsed); } catch (caughtError) { setError( caughtError instanceof Error ? caughtError.message : "Could not load leadership transfers.", ); } finally { setLoading(false); } }, []); useEffect(() => { void load(); }, [load]); const pending = useMemo(() => (docs ?? []).filter((doc) => doc.status === "pending"), [docs]); const mine = useMemo( () => (docs ?? []).filter((doc) => initiatorId(doc.initiator) === currentUserId), [currentUserId, docs], ); const resolved = useMemo(() => (docs ?? []).filter((doc) => doc.status !== "pending"), [docs]); async function handleCancel(transferId: number) { setCancellingId(transferId); try { const result = await cancelDivisionRemoval(transferId); if (result.success) { toast.success("Removal request cancelled"); void load(); } else { toast.error(result.error ?? "Could not cancel the removal request."); } } finally { setCancellingId(null); } } // Regular members: hide the section entirely while loading, on fetch errors, // and whenever they have no requests of their own. if (!canResolve) { if (docs === null || mine.length === 0) return null; return (
{mine.map((doc) => (
{STATUS_LABELS[doc.status]} Removal of {personRefLabel(doc.subject)} from{" "} {assignmentRef(doc.fromAssignment)} to {assignmentRef(doc.toAssignment)} requested {formatTransferDate(doc.createdAt)} {doc.status === "pending" && ( )}
))}
); } if (docs === null && loading) { return (
); } if (docs === null && error) { return ( Leadership transfers unavailable {error} ); } return (
{error && ( Could not refresh leadership transfers {error} )} {pending.length === 0 ? ( No pending removal requests need a final call. ) : (
{pending.map((doc) => ( ))}
)} {resolved.length > 0 && ( {resolved.map((doc) => (
{personRefLabel(doc.initiator)} removed {personRefLabel(doc.subject)} from {assignmentRef(doc.fromAssignment)} to {assignmentRef(doc.toAssignment)} {STATUS_LABELS[doc.status]} {doc.resolvedAt && ( {formatTransferDate(doc.resolvedAt)} )}
))}
)}
); }