diff --git a/src/app/page.tsx b/src/app/page.tsx index 07001d7..b21f979 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,7 +18,7 @@ import { type MatchmakerLogSource, } from "@/lib/matchmaker-log-source"; import { readMatchmakerLogFile } from "@/lib/matchmaker-logs-server"; -import { fetchEntryHoldPerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger"; +import { fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger"; import type { AdminMatchRow, DbMatch, @@ -144,21 +144,25 @@ export default async function Home({ matchesError = matchesRes.error.message; } else { const rawMatches = (matchesRes.data ?? []) as DbMatch[]; - const holdByMatch = await fetchEntryHoldPerPlayerCoinsByMatchIds( - supabase, - rawMatches - .map((m) => Number(m.id)) - .filter((id) => Number.isFinite(id)), - ); + const { holdPerPlayer, feePerPlayer } = + await fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds( + supabase, + rawMatches + .map((m) => Number(m.id)) + .filter((id) => Number.isFinite(id)), + ); matches = rawMatches.map((m) => { const idNum = Number(m.id); - const hold = Number.isFinite(idNum) - ? holdByMatch.get(idNum) - : undefined; + const hold = + Number.isFinite(idNum) ? holdPerPlayer.get(idNum) : undefined; + const fee = + Number.isFinite(idNum) ? feePerPlayer.get(idNum) : undefined; return { ...m, entryHoldPerPlayerCoins: hold !== undefined ? hold.toString() : null, + entryFeePerPlayerCoins: + fee !== undefined ? fee.toString() : null, }; }); } diff --git a/src/components/admin-dashboard.tsx b/src/components/admin-dashboard.tsx index bd0d0d0..7963363 100644 --- a/src/components/admin-dashboard.tsx +++ b/src/components/admin-dashboard.tsx @@ -66,11 +66,6 @@ function formatTs(value: string | null): string { } } -function matchEntryFeeCoinString(v: DbMatch["entry_fee"]): string | null { - if (v == null) return null; - return txAmountToBigInt(v).toString(); -} - function formatPrizeCcChip(v: DbMatch["prize_cc"]): string { if (v == null) return "—"; const b = txAmountToBigInt(v); @@ -819,7 +814,7 @@ export function AdminDashboard({ matchId={m.id} statusLabel={statusLabel(m.status)} createdAtLabel={formatTs(m.created_at)} - entryFeeCoins={matchEntryFeeCoinString(m.entry_fee)} + entryFeeCoins={m.entryFeePerPlayerCoins} entryHoldPerPlayerCoins={m.entryHoldPerPlayerCoins} prizeCcLabel={formatPrizeCcChip(m.prize_cc)} winnerId={m.winner_id} diff --git a/src/components/match-history-battle-card.tsx b/src/components/match-history-battle-card.tsx index 0e57324..c59db79 100644 --- a/src/components/match-history-battle-card.tsx +++ b/src/components/match-history-battle-card.tsx @@ -4,6 +4,40 @@ import Link from "next/link"; import { ClickableUserId } from "@/components/clickable-user-id"; import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc"; +/** Ledger rows are per player; both players pay → show `1.0 RC x 2 = 2.0 RC`. */ +function rcPerPlayerTimesTwoLine(coinStr: string | null): string { + if (coinStr == null || String(coinStr).trim() === "") return "—"; + try { + const perPlayer = BigInt(String(coinStr).trim()); + const bothPlayers = perPlayer * BigInt(2); + return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`; + } catch { + return "—"; + } +} + +function entryCombinedPerPlayerTimesTwoLine( + feeCoins: string | null, + holdCoins: string | null, +): string { + if ( + feeCoins == null || + holdCoins == null || + String(feeCoins).trim() === "" || + String(holdCoins).trim() === "" + ) { + return "—"; + } + try { + const perPlayer = + BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim()); + const bothPlayers = perPlayer * BigInt(2); + return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`; + } catch { + return "—"; + } +} + type PlayerSide = { id: number | null; username: string | null; @@ -16,7 +50,7 @@ type Props = { matchId: number; statusLabel: string; createdAtLabel: string; - /** `matches.entry_fee` as coin string (bigint); RC shown in footer. */ + /** Ledger `entry_fee` / player as coin string; null if unknown. */ entryFeeCoins: string | null; /** Ledger `entry_hold` / player as coin string; null if unknown. */ entryHoldPerPlayerCoins: string | null; @@ -28,35 +62,6 @@ type Props = { winnerId: number | string | null; }; -function rcLabelFromCoinString(s: string | null): string { - if (s == null || String(s).trim() === "") return "—"; - try { - return formatRcLabelFromCoinsBigInt(BigInt(String(s).trim())); - } catch { - return "—"; - } -} - -function entryTotalRcLabel( - feeCoins: string | null, - holdCoins: string | null, -): string { - if ( - feeCoins == null || - holdCoins == null || - String(feeCoins).trim() === "" || - String(holdCoins).trim() === "" - ) { - return "—"; - } - try { - const sum = BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim()); - return formatRcLabelFromCoinsBigInt(sum); - } catch { - return "—"; - } -} - function idsMatch( a: number | string | null | undefined, b: number | string | null | undefined, @@ -185,9 +190,12 @@ export function MatchHistoryBattleCard({ right, winnerId, }: Props) { - const entryRc = entryTotalRcLabel(entryFeeCoins, entryHoldPerPlayerCoins); - const entryFeeRc = rcLabelFromCoinString(entryFeeCoins); - const entryHoldRc = rcLabelFromCoinString(entryHoldPerPlayerCoins); + const entryLine = entryCombinedPerPlayerTimesTwoLine( + entryFeeCoins, + entryHoldPerPlayerCoins, + ); + const entryHoldLine = rcPerPlayerTimesTwoLine(entryHoldPerPlayerCoins); + const entryFeeLine = rcPerPlayerTimesTwoLine(entryFeeCoins); const prizeCcDisplay = prizeCcLabel.trim() === "" ? "—" : prizeCcLabel; const hasWinner = winnerId != null && @@ -238,16 +246,16 @@ export function MatchHistoryBattleCard({ Time {createdAtLabel} - Entry {entryRc} + Entry {entryLine} - Entry hold {entryHoldRc} + Entry hold {entryHoldLine} Prize {prizeCcDisplay} CC - - Entry fee {entryFeeRc} + + Entry fee {entryFeeLine} ; + counts: Map; +}; + +function bumpAgg(agg: PerRemarkAgg, matchId: number, amount: bigint): void { + agg.sums.set(matchId, (agg.sums.get(matchId) ?? BigInt(0)) + amount); + agg.counts.set(matchId, (agg.counts.get(matchId) ?? 0) + 1); +} + +function avgPerMatch(agg: PerRemarkAgg): Map { + const out = new Map(); + for (const [mid, sum] of agg.sums) { + const c = agg.counts.get(mid) ?? 0; + if (c > 0) out.set(mid, sum / BigInt(c)); + } + return out; +} + +export type MatchEntryHoldAndFeePerPlayerCoins = { + holdPerPlayer: Map; + feePerPlayer: Map; +}; + /** - * For each match_id, average `entry_hold` debit amount (same per player when - * both rows exist; sum/count handles odd counts). + * For each match_id, average debit amount for `entry_hold` and `entry_fee` + * (per player when two rows exist; sum/count handles odd counts). */ -export async function fetchEntryHoldPerPlayerCoinsByMatchIds( +export async function fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds( supabase: SupabaseClient, matchIds: number[], -): Promise> { - const out = new Map(); - if (matchIds.length === 0) return out; +): Promise { + const empty: MatchEntryHoldAndFeePerPlayerCoins = { + holdPerPlayer: new Map(), + feePerPlayer: new Map(), + }; + if (matchIds.length === 0) return empty; const { data, error } = await supabase .from("transactions") @@ -23,19 +50,24 @@ export async function fetchEntryHoldPerPlayerCoinsByMatchIds( .in("match_id", matchIds); if (error) { - return out; + return empty; } - const sums = new Map(); - const counts = new Map(); + const holdAgg: PerRemarkAgg = { + sums: new Map(), + counts: new Map(), + }; + const feeAgg: PerRemarkAgg = { + sums: new Map(), + counts: new Map(), + }; for (const row of (data ?? []) as Pick< DbTransaction, "match_id" | "amount" | "remarks" >[]) { - if (normRemark(row.remarks) !== "entry_hold") { - continue; - } + const rmk = normRemark(row.remarks); + if (rmk !== "entry_hold" && rmk !== "entry_fee") continue; const midRaw = row.match_id; if (midRaw == null) continue; const mid = @@ -43,14 +75,12 @@ export async function fetchEntryHoldPerPlayerCoinsByMatchIds( if (!Number.isFinite(mid)) continue; const a = txAmountToBigInt(row.amount); if (a === BigInt(0)) continue; - sums.set(mid, (sums.get(mid) ?? BigInt(0)) + a); - counts.set(mid, (counts.get(mid) ?? 0) + 1); + if (rmk === "entry_hold") bumpAgg(holdAgg, mid, a); + else bumpAgg(feeAgg, mid, a); } - for (const [mid, sum] of sums) { - const c = counts.get(mid) ?? 0; - if (c > 0) out.set(mid, sum / BigInt(c)); - } - - return out; + return { + holdPerPlayer: avgPerMatch(holdAgg), + feePerPlayer: avgPerMatch(feeAgg), + }; } diff --git a/src/types/database.ts b/src/types/database.ts index b46d159..0d26cb4 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -29,6 +29,8 @@ export type DbMatch = { export type AdminMatchRow = DbMatch & { /** Per-player `entry_hold` debit in coins (stringified bigint); null if unknown. */ entryHoldPerPlayerCoins: string | null; + /** Per-player `entry_fee` debit in coins from `transactions`; null if unknown. */ + entryFeePerPlayerCoins: string | null; }; /** Mirrors `public.settings` (key/value config rows). */