"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ClickableUserId } from "@/components/clickable-user-id"; import { AdminLedger } from "@/components/admin-ledger"; import { MatchHistoryBattleCard } from "@/components/match-history-battle-card"; import type { DashboardStatsBundle, LeaderboardRow } from "@/lib/dashboard-stats"; import { formatRcBalanceWithCoins } from "@/lib/coins-rc"; import { MatchmakerLogTerminal } from "@/components/matchmaker-log-terminal"; import { buildDashboardHref, type AdminDashboardTab, } from "@/lib/dashboard-search-url"; import type { MatchmakerLogSource } from "@/lib/matchmaker-log-source"; import type { AdminMatchRow, DbMatch, DbTransaction, DbUser, } from "@/types/database"; import { txAmountToBigInt, type SerializedLedgerGlobalSummary, } from "@/lib/ledger-integrity"; import type { LedgerSortKey, LedgerSortOrder } from "@/lib/ledger-table-view"; /** Bigint columns often arrive as strings from PostgREST / JSON. */ function matchHasRecordedWinner(winnerId: DbMatch["winner_id"]): boolean { if (winnerId == null) return false; if (typeof winnerId === "number") { return Number.isInteger(winnerId) && winnerId > 0; } if (typeof winnerId === "string") { const t = winnerId.trim(); if (t === "" || !/^-?\d+$/.test(t)) return false; const n = Number(t); return Number.isFinite(n) && n > 0; } return false; } function participantInMatch(m: DbMatch, participantId: number): boolean { const red = m.user_red; const blue = m.user_blue; if (red != null && Number(red) === participantId) return true; if (blue != null && Number(blue) === participantId) return true; return false; } function coalesceUserId(v: number | string | null): number | null { if (v == null) return null; const n = Number(v); return Number.isFinite(n) ? Math.trunc(n) : null; } /** UTC ISO slice — identical on server and client (avoids hydration mismatch from `toLocaleString`). */ function formatTs(value: string | null): string { if (!value) return "—"; try { const d = new Date(value); if (Number.isNaN(d.getTime())) return value; return d.toISOString().replace("T", " ").slice(0, 19) + " UTC"; } catch { return value; } } function formatPrizeCcChip(v: DbMatch["prize_cc"]): string { if (v == null) return "—"; const b = txAmountToBigInt(v); try { return b.toLocaleString("en-US"); } catch { return String(v); } } type LeaderboardSortKey = "rank" | "player" | "rc" | "winRate" | "wins"; function playerSortKey(row: LeaderboardRow): string { const t = row.username?.trim(); if (t) return t.toLowerCase(); return "\uffff"; } function leaderboardDefaultSortDir(key: LeaderboardSortKey): "asc" | "desc" { if (key === "player" || key === "rank") return "asc"; return "desc"; } function sortLeaderboardRows( rows: LeaderboardRow[], key: LeaderboardSortKey, dir: "asc" | "desc", ): LeaderboardRow[] { const out = [...rows]; out.sort((a, b) => { let cmp = 0; switch (key) { case "rank": cmp = a.winsLeaderboardRank - b.winsLeaderboardRank; break; case "wins": cmp = a.matchesWon - b.matchesWon; break; case "player": { const ka = playerSortKey(a); const kb = playerSortKey(b); cmp = ka.localeCompare(kb, "en", { sensitivity: "base" }); if (cmp === 0) cmp = a.userId - b.userId; break; } case "rc": { const aOk = a.rcBalance != null && Number.isFinite(a.rcBalance); const bOk = b.rcBalance != null && Number.isFinite(b.rcBalance); if (!aOk && !bOk) cmp = 0; else if (!aOk) cmp = 1; else if (!bOk) cmp = -1; else cmp = a.rcBalance! - b.rcBalance!; break; } case "winRate": { const aOk = a.winRatePercent != null && Number.isFinite(a.winRatePercent); const bOk = b.winRatePercent != null && Number.isFinite(b.winRatePercent); if (!aOk && !bOk) cmp = 0; else if (!aOk) cmp = 1; else if (!bOk) cmp = -1; else cmp = a.winRatePercent! - b.winRatePercent!; break; } default: break; } if (cmp !== 0) return dir === "asc" ? cmp : -cmp; return a.userId - b.userId; }); return out; } function LeaderboardSortTh({ label, sortKey, activeKey, activeDir, onActivate, align = "left", }: { label: string; sortKey: LeaderboardSortKey; activeKey: LeaderboardSortKey; activeDir: "asc" | "desc"; onActivate: (key: LeaderboardSortKey) => void; align?: "left" | "right"; }) { const active = activeKey === sortKey; const rowAlign = align === "right" ? "text-right" : "text-left"; const btnAlign = align === "right" ? "justify-end text-right" : "justify-start text-left"; return ( ); } type Props = { users: DbUser[]; matches: AdminMatchRow[]; usersError: string | null; matchesError: string | null; transactionsError: string | null; /** Ledger tab: full-database totals + user nets (server scan). */ ledgerGlobalSummary: SerializedLedgerGlobalSummary | null; /** Ledger tab: rows in the selected UTC date range (full set for audits). */ ledgerTransactionsFiltered: DbTransaction[]; /** Ledger tab: current page of the ledger table (sorted + paginated). */ ledgerTableRows: DbTransaction[]; ledgerTotalRowsInRange: number; ledgerPage: number; ledgerPageSize: number; ledgerTotalPages: number; ledgerSort: LedgerSortKey; ledgerOrder: LedgerSortOrder; ledgerFrom: string; ledgerTo: string; statsBundle: DashboardStatsBundle | null; /** From server `searchParams` so SSR and client markup match (do not use `useSearchParams` here). */ tab: AdminDashboardTab; highlightId: string | null; participantRaw: string | null; matchmakerSource: MatchmakerLogSource; matchmakerContent: string; matchmakerError: string | null; }; function StatCard({ title, value, }: { title: string; value: number | null; }) { const display = value == null ? "—" : value.toLocaleString("en-US"); return (

{title}

{display}

); } export function AdminDashboard({ users, matches, usersError, matchesError, transactionsError, ledgerGlobalSummary, ledgerTransactionsFiltered, ledgerTableRows, ledgerTotalRowsInRange, ledgerPage, ledgerPageSize, ledgerTotalPages, ledgerSort, ledgerOrder, ledgerFrom, ledgerTo, statsBundle, tab, highlightId, participantRaw, matchmakerSource, matchmakerContent, matchmakerError, }: Props) { const [hideNoWinner, setHideNoWinner] = useState(true); const [lbSortKey, setLbSortKey] = useState("wins"); const [lbSortDir, setLbSortDir] = useState<"asc" | "desc">("desc"); const lbRaw = statsBundle?.leaderboard; const sortedLeaderboard = useMemo(() => { const rows = lbRaw ?? []; if (rows.length === 0) return []; return sortLeaderboardRows(rows, lbSortKey, lbSortDir); }, [lbRaw, lbSortKey, lbSortDir]); const onLbSort = useCallback((key: LeaderboardSortKey) => { setLbSortKey((prevKey) => { if (key === prevKey) { setLbSortDir((d) => (d === "asc" ? "desc" : "asc")); return prevKey; } setLbSortDir(leaderboardDefaultSortDir(key)); return key; }); }, []); const userById = useMemo(() => { const m = new Map(); for (const u of users) { m.set(u.id, u); } return m; }, [users]); const usernameById = useMemo(() => { const m = new Map(); for (const u of users) { m.set(u.id, u.username); } return m; }, [users]); const participantId = useMemo(() => { if (!participantRaw) return null; const n = Number(participantRaw); return Number.isFinite(n) ? n : null; }, [participantRaw]); const filteredMatches = useMemo(() => { if (participantId == null) return matches; return matches.filter((m) => participantInMatch(m, participantId)); }, [matches, participantId]); const visibleMatches = useMemo(() => { if (!hideNoWinner) return filteredMatches; return filteredMatches.filter((m) => matchHasRecordedWinner(m.winner_id)); }, [filteredMatches, hideNoWinner]); useEffect(() => { if (tab !== "players" || !highlightId) return; const el = document.getElementById(`player-row-${highlightId}`); el?.scrollIntoView({ block: "center", behavior: "smooth" }); }, [tab, highlightId]); const tabClass = (active: boolean) => [ "inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium transition", active ? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900" : "bg-zinc-100 text-zinc-700 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700", ].join(" "); function editHref(u: DbUser): string { return buildDashboardHref({ tab, highlightId, participantRaw, editId: u.id, }); } const totalUsersLabel = statsBundle?.stats?.totalUsers ?? users.length; const totalMatchesLabel = statsBundle?.stats?.totalMatches ?? matches.length; const statusLabel = (status: number | null) => { if (status === 2) return "Final"; if (status === 1) return "Live"; if (status === 0) return "Waiting"; return status == null ? "Unknown" : `Status ${status}`; }; return (
Overview Players ( {totalUsersLabel.toLocaleString("en-US")} {!statsBundle?.stats ? ` · table ${users.length.toLocaleString("en-US")}` : null} ) Matches ( {totalMatchesLabel.toLocaleString("en-US")} {!statsBundle?.stats ? ` · table ${matches.length.toLocaleString("en-US")}` : null} ) Matchmaker Ledger Settings
{tab === "dashboard" ? (
{statsBundle?.error ? (
{statsBundle.error}
) : null}

Active players

Matches

Top players by match wins

{(statsBundle?.leaderboard ?? []).length === 0 ? ( ) : ( sortedLeaderboard.map((row, i) => ( )) )}
No wins recorded yet (no rows with a winner).
{i + 1} {row.username != null && row.username !== "" ? ( {" "} ({row.username}) ) : ( (—) )} {formatRcBalanceWithCoins(row.rcBalance)} {row.winRatePercent == null ? "—" : `${row.winRatePercent.toLocaleString("en-US", { maximumFractionDigits: 1, minimumFractionDigits: 0, })}%`} {row.matchesWon.toLocaleString("en-US")}
) : tab === "players" ? (
{usersError ? (

{usersError}

) : (
{users.length === 0 ? ( ) : ( users.map((u) => { const isHi = highlightId != null && highlightId === String(u.id); return ( ); }) )}
ID Username CC RC Created Last seen Actions
No players yet.
{u.username ?? "—"} {u.cc ?? "—"} {u.rc ?? "—"} {formatTs(u.created_at)} {formatTs(u.last_logged_at)}
Edit Show matches
)}
) : tab === "matchmaker" ? (

history.log {" "} is the processed summary;{" "} matchmaker.log {" "} is raw output. Toggle below or{" "} open fullscreen .

) : tab === "ledger" ? ( ) : (
{participantId != null ? (
Showing matches where{" "} {participantId} {usernameById.get(participantId) != null && usernameById.get(participantId) !== "" ? ( {" "} ({usernameById.get(participantId)}) ) : null}{" "} is the Red or Blue user ( {filteredMatches.length} of {matches.length}) Clear filter
) : null} {matchesError ? (

{matchesError}

) : (
Showing {visibleMatches.length} of {filteredMatches.length}
{visibleMatches.length === 0 ? (
{filteredMatches.length === 0 ? "No matches yet." : hideNoWinner ? "No matches left after hiding no-winner matches." : "No matches for this filter."}
) : ( visibleMatches.map((m) => { const redId = coalesceUserId(m.user_red); const blueId = coalesceUserId(m.user_blue); const leftUser = redId != null ? userById.get(redId) : null; const rightUser = blueId != null ? userById.get(blueId) : null; return ( ); }) )}
)}
)}
); }