"use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { ClickableUserId } from "@/components/clickable-user-id"; import { buildDashboardHref } from "@/lib/dashboard-search-url"; import { formatRcBalanceWithCoins, formatRcLabelFromCoinsBigInt, } from "@/lib/coins-rc"; import { auditMatchEconomics } from "@/lib/ledger-match-audit"; import { computeLedgerIntegrity, parseSerializedLedgerGlobalSummary, txAmountToBigInt, type LedgerIntegrity, type SerializedLedgerGlobalSummary, } from "@/lib/ledger-integrity"; import { DEFAULT_LEDGER_PAGE_SIZE, LEDGER_PAGE_SIZE_OPTIONS, ledgerPaginationItems, type LedgerSortKey, type LedgerSortOrder, } from "@/lib/ledger-table-view"; import { buildLedgerScopedDailyCumulativeSeries } from "@/lib/ledger-scoped-daily-series"; import { addSystemSupply } from "@/app/actions/add-system-supply"; import { LedgerRangeCumulativeChart } from "@/components/ledger-range-cumulative-chart"; import type { DbTransaction, DbUser } from "@/types/database"; function emptyIntegrity(): LedgerIntegrity { const z = BigInt(0); return { mintTotal: z, sinkTotal: z, entryFeeCoinsTotal: z, purchaseCoinsTotal: z, entryHoldCoinsTotal: z, rewardCoinsTotal: z, sumUserNets: z, balanced: true, totalDebitVolume: z, totalCreditVolume: z, volumeBalanced: true, }; } function formatTsUtc(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 formatBigAmount(n: bigint): string { return n.toLocaleString("en-US"); } function remarkClass(remarks: string | null): string { const r = (remarks ?? "").toLowerCase(); if (r === "supply") return "bg-emerald-100 text-emerald-900 dark:bg-emerald-950/70 dark:text-emerald-100"; if (r === "purchase") return "bg-sky-100 text-sky-900 dark:bg-sky-950/70 dark:text-sky-100"; if (r === "entry_hold") return "bg-amber-100 text-amber-950 dark:bg-amber-950/60 dark:text-amber-100"; if (r === "entry_fee") return "bg-violet-100 text-violet-900 dark:bg-violet-950/60 dark:text-violet-100"; if (r === "reward") return "bg-rose-100 text-rose-900 dark:bg-rose-950/60 dark:text-rose-100"; return "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200"; } type Props = { ledgerGlobalSummary: SerializedLedgerGlobalSummary | null; transactionsScoped: DbTransaction[]; ledgerTableRows: DbTransaction[]; ledgerTotalRowsInRange: number; ledgerPage: number; ledgerPageSize: number; ledgerTotalPages: number; ledgerSort: LedgerSortKey; ledgerOrder: LedgerSortOrder; users: DbUser[]; error: string | null; highlightId: string | null; participantRaw: string | null; ledgerFrom: string; ledgerTo: string; supplyError: boolean; readOnly: boolean; }; export function AdminLedger({ ledgerGlobalSummary, transactionsScoped, ledgerTableRows, ledgerTotalRowsInRange, ledgerPage, ledgerPageSize, ledgerTotalPages, ledgerSort, ledgerOrder, users, error, highlightId, participantRaw, ledgerFrom, ledgerTo, supplyError, readOnly, }: Props) { const usernameById = useMemo(() => { const m = new Map(); for (const u of users) { m.set(u.id, u.username); } return m; }, [users]); const systemAccount = useMemo(() => { for (const u of users) { if (Number(u.id) === 1) return u; } return null; }, [users]); const { integrityGlobal, userNets } = useMemo(() => { if (ledgerGlobalSummary == null) { return { integrityGlobal: emptyIntegrity(), userNets: [] as [number, bigint][], }; } const { integrity, userNets } = parseSerializedLedgerGlobalSummary(ledgerGlobalSummary); return { integrityGlobal: integrity, userNets }; }, [ledgerGlobalSummary]); const integrityScoped = useMemo( () => computeLedgerIntegrity(transactionsScoped), [transactionsScoped], ); const scopedDailyCumulative = useMemo( () => buildLedgerScopedDailyCumulativeSeries( transactionsScoped, ledgerFrom, ledgerTo, ), [transactionsScoped, ledgerFrom, ledgerTo], ); const matchChecks = useMemo( () => auditMatchEconomics(transactionsScoped), [transactionsScoped], ); function ledgerHref( override: Partial<{ ledgerPage: number; ledgerSort: LedgerSortKey; ledgerOrder: LedgerSortOrder; ledgerPageSize: number; }>, ): string { return buildDashboardHref({ tab: "ledger", highlightId, participantRaw, ledgerFrom, ledgerTo, ledgerPage: override.ledgerPage ?? ledgerPage, ledgerSort: override.ledgerSort ?? ledgerSort, ledgerOrder: override.ledgerOrder ?? ledgerOrder, ledgerPageSize: override.ledgerPageSize ?? ledgerPageSize, }); } function sortToggleHref(column: LedgerSortKey): string { const nextOrder: LedgerSortOrder = ledgerSort === column ? ledgerOrder === "asc" ? "desc" : "asc" : "asc"; return ledgerHref({ ledgerSort: column, ledgerOrder: nextOrder, ledgerPage: 1, }); } function sortHeaderClass(column: LedgerSortKey): string { return [ "whitespace-nowrap px-4 py-3 font-medium transition", ledgerSort === column ? "text-sky-800 dark:text-sky-200" : "text-zinc-600 dark:text-zinc-400", ].join(" "); } function userCell(id: number | null, role: "from" | "to") { if (id == null) { return ( {role === "from" ? "∅ mint" : "∅"} ); } const un = usernameById.get(id); return ( {un != null && un !== "" ? ( ({un}) ) : null} ); } const feeRcLabelGlobal = formatRcLabelFromCoinsBigInt( integrityGlobal.entryFeeCoinsTotal, ); const purchaseRcLabelGlobal = formatRcLabelFromCoinsBigInt( integrityGlobal.purchaseCoinsTotal, ); const entryHoldRcLabelGlobal = formatRcLabelFromCoinsBigInt( integrityGlobal.entryHoldCoinsTotal, ); const feeRcLabelScoped = formatRcLabelFromCoinsBigInt( integrityScoped.entryFeeCoinsTotal, ); const purchaseRcLabelScoped = formatRcLabelFromCoinsBigInt( integrityScoped.purchaseCoinsTotal, ); const entryHoldRcLabelScoped = formatRcLabelFromCoinsBigInt( integrityScoped.entryHoldCoinsTotal, ); const holdRewardMismatch = integrityScoped.entryHoldCoinsTotal !== integrityScoped.rewardCoinsTotal; const pageStart = ledgerTotalRowsInRange === 0 ? 0 : (ledgerPage - 1) * ledgerPageSize + 1; const pageEnd = Math.min( ledgerPage * ledgerPageSize, ledgerTotalRowsInRange, ); const pageNavItems = ledgerPaginationItems(ledgerPage, ledgerTotalPages); const [supplyModalOpen, setSupplyModalOpen] = useState(false); useEffect(() => { if (supplyError) { setSupplyModalOpen(true); } }, [supplyError]); useEffect(() => { if (!supplyModalOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { setSupplyModalOpen(false); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [supplyModalOpen]); const navLinkClass = "rounded px-2 py-1 font-medium text-sky-700 underline decoration-sky-400/50 underline-offset-2 hover:bg-sky-50 dark:text-sky-300 dark:hover:bg-sky-950/40"; const navMutedClass = "rounded px-2 py-1 text-zinc-400 dark:text-zinc-500"; return (

Ledger book {" — "} The ledger table and match checks use the selected UTC range (default last 30 days). User nets, Σ issuance, and the three all-time fee / purchase / hold cards are computed from{" "} every transaction row. The same metrics for the date range only appear under the filter.

{error ? (

{error}

) : null}

Σ user nets vs net issued

All transactions

{!readOnly ? ( ) : null}

{formatBigAmount(integrityGlobal.sumUserNets)}

={" "} {formatBigAmount( integrityGlobal.mintTotal - integrityGlobal.sinkTotal, )}

{integrityGlobal.balanced ? "Balanced ✓" : "Mismatch — inspect data"}

Fee collected (entry_fee)

All-time

{feeRcLabelGlobal}

{formatBigAmount(integrityGlobal.entryFeeCoinsTotal)} coins

Total purchase

All-time

{purchaseRcLabelGlobal}

{formatBigAmount(integrityGlobal.purchaseCoinsTotal)} coins

Total entry_hold / reward

All-time

{entryHoldRcLabelGlobal}

{formatBigAmount(integrityGlobal.entryHoldCoinsTotal)} coins

User net (credits − debits)

All transactions

{userNets.length === 0 ? ( ) : ( userNets.map(([uid, bal]) => ( )) )}
User Net (coins) Net (RC)
No non-zero balances in loaded rows.
{userCell(uid, "from")} BigInt(0) ? "text-emerald-700 dark:text-emerald-300" : bal < BigInt(0) ? "text-rose-700 dark:text-rose-300" : "", ].join(" ")} > {bal > BigInt(0) ? "+" : ""} {formatBigAmount(bal)} {bal < BigInt(0) ? `−${formatRcLabelFromCoinsBigInt(-bal)}` : formatRcLabelFromCoinsBigInt(bal)}
{highlightId ? ( ) : null} {participantRaw ? ( ) : null} {ledgerPageSize !== DEFAULT_LEDGER_PAGE_SIZE ? ( ) : null}
Reset range

In selected range ( {ledgerFrom}–{ledgerTo} {" "} UTC)

Fee collected (entry_fee)

{feeRcLabelScoped}

{formatBigAmount(integrityScoped.entryFeeCoinsTotal)} coins

Total purchase

{purchaseRcLabelScoped}

{formatBigAmount(integrityScoped.purchaseCoinsTotal)} coins

Total entry_hold / reward

{entryHoldRcLabelScoped}

{formatBigAmount(integrityScoped.entryHoldCoinsTotal)} coins

{holdRewardMismatch ? (
Data mismatch: entry_hold total ( {formatBigAmount(integrityScoped.entryHoldCoinsTotal)} coins) does not equal{" "} reward total ( {formatBigAmount(integrityScoped.rewardCoinsTotal)} coins).
) : null} {!integrityScoped.volumeBalanced ? (
Debit / credit volume: debits + mint ( {formatBigAmount( integrityScoped.totalDebitVolume + integrityScoped.mintTotal, )} ) ≠ credits + null-{`to`} outflows ( {formatBigAmount( integrityScoped.totalCreditVolume + integrityScoped.sinkTotal, )} ). This usually means inconsistent rows (e.g. both {`from`} and{" "} {`to`} null with non-zero amount).
) : null}

Ledger

{ledgerTotalRowsInRange.toLocaleString("en-US")} rows in range{" "} {ledgerFrom}–{ledgerTo} {" "} UTC (cap 10,000) · default sort newest first; click a column header to sort

Rows per page {LEDGER_PAGE_SIZE_OPTIONS.map((n) => n === ledgerPageSize ? ( {n} ) : ( {n} ), )}

{ledgerTotalRowsInRange === 0 ? ( ) : ( ledgerTableRows.map((row) => { const amt = txAmountToBigInt(row.amount); return ( ); }) )}
ID {ledgerSort === "id" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} Created {ledgerSort === "created_at" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} From {ledgerSort === "from" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} To {ledgerSort === "to" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} Amount {ledgerSort === "amount" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} RC {ledgerSort === "rc" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} Remarks {ledgerSort === "remarks" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null} Match {ledgerSort === "match_id" ? ( {ledgerOrder === "asc" ? "▲" : "▼"} ) : null}
No transactions loaded.
{row.id} {formatTsUtc(row.created_at)} {userCell(row.from, "from")} {userCell(row.to, "to")} {formatBigAmount(amt)} {formatRcLabelFromCoinsBigInt(amt)} {row.remarks ? ( {row.remarks} ) : ( "—" )} {row.match_id != null ? ( {row.match_id} ) : ( "—" )}
{ledgerTotalRowsInRange > 0 ? (

Rows {pageStart.toLocaleString("en-US")}– {pageEnd.toLocaleString("en-US")} of{" "} {ledgerTotalRowsInRange.toLocaleString("en-US")} · Page{" "} {ledgerPage.toLocaleString("en-US")} /{" "} {ledgerTotalPages.toLocaleString("en-US")}

) : null}
{matchChecks.length > 0 ? (

By match_id (2 entry_hold + 2 entry_fee + 1 reward)

{matchChecks.map((m) => (
#{m.matchId} · log · {m.balanced ? ( balanced ) : ( {m.rewardDeltaCoins > BigInt(0) ? `overspent +${formatBigAmount(m.rewardDeltaCoins)}` : m.rewardDeltaCoins < BigInt(0) ? `short reward ${formatBigAmount(-m.rewardDeltaCoins)}` : "structure mismatch"} )} · h:{m.entryHoldCount} f:{m.entryFeeCount} r:{m.rewardCount}
))}
) : null} {supplyModalOpen && !readOnly ? (
setSupplyModalOpen(false)} >
e.stopPropagation()} >

Add system supply

Credits user id{" "} 1 {systemAccount?.username ? ( {" "} ({systemAccount.username}) ) : null} : mint row (to = 1,{" "} remarks = supply), RC increases by the same coin amount.

Current RC:{" "} {systemAccount ? formatRcBalanceWithCoins(systemAccount.rc) : "— (user 1 not in loaded set)"}

{supplyError ? (

Could not add supply. Use a positive whole number of coin units (no decimals), and ensure the database and service key are available.

) : null}
) : null}
); }