import { Fragment } from "react"; import type { Metadata } from "next"; import Link from "next/link"; import { redirect } from "next/navigation"; import { AdminHeader } from "@/components/admin-header"; import { logPageAccess } from "@/lib/auth/log-page-access"; import { requirePageRead } from "@/lib/auth/require-session"; import { formatRcDecimalFromCoinsBigInt, formatRcLabelFromCoinsBigInt, formatSignedRcFromCoinsBigInt, } from "@/lib/coins-rc"; import { auditMatchEconomics } from "@/lib/ledger-match-audit"; import { buildLedgerBookPairs } from "@/lib/ledger-book-lines"; import { createAdminSupabase } from "@/lib/supabase/admin"; import type { DbTransaction } from "@/types/database"; export const dynamic = "force-dynamic"; export async function generateMetadata(): Promise { return { title: "Ledger book · Kick Kings Admin", }; } function firstSearchParam( v: string | string[] | undefined, ): string | null { if (v === undefined) return null; if (typeof v === "string") return v; return v[0] ?? null; } /** Previous calendar month in UTC, as inclusive YYYY-MM-DD bounds. */ function utcLastMonthDateRange(): { from: string; to: string } { const now = new Date(); const y = now.getUTCFullYear(); const m = now.getUTCMonth(); const yearLast = m === 0 ? y - 1 : y; const monthLast = m === 0 ? 11 : m - 1; const fromDate = new Date(Date.UTC(yearLast, monthLast, 1)); const toDate = new Date(Date.UTC(yearLast, monthLast + 1, 0)); const from = fromDate.toISOString().slice(0, 10); const to = toDate.toISOString().slice(0, 10); return { from, to }; } function parseIsoDateOnly(s: string): Date | null { if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null; const d = new Date(`${s}T00:00:00.000Z`); return Number.isNaN(d.getTime()) ? null : d; } type UsernameRow = { id: number; username: string | null }; export default async function LedgerBookPage({ searchParams, }: { searchParams: Promise<{ from?: string | string[]; to?: string | string[] }>; }) { const account = await requirePageRead("ledger"); await logPageAccess(account, "ledger-book"); const sp = await searchParams; let fromStr = firstSearchParam(sp.from); let toStr = firstSearchParam(sp.to); const defaults = utcLastMonthDateRange(); if (!fromStr || !toStr) { redirect( `/ledger-book?from=${encodeURIComponent(defaults.from)}&to=${encodeURIComponent(defaults.to)}`, ); } const fromDate = parseIsoDateOnly(fromStr); const toDate = parseIsoDateOnly(toStr); if (!fromDate || !toDate) { redirect( `/ledger-book?from=${encodeURIComponent(defaults.from)}&to=${encodeURIComponent(defaults.to)}`, ); } if (fromStr > toStr) { const t = fromStr; fromStr = toStr; toStr = t; } const rangeStartIso = `${fromStr}T00:00:00.000Z`; const rangeEndIso = `${toStr}T23:59:59.999Z`; const supabase = createAdminSupabase(); let configError: string | null = null; let loadError: string | null = null; let transactions: DbTransaction[] = []; let userNameRows: UsernameRow[] = []; if (!supabase) { configError = "Set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in .env.local."; } else { const [txRes, usersRes] = await Promise.all([ supabase .from("transactions") .select("id, created_at, from, to, amount, remarks, match_id") .gte("created_at", rangeStartIso) .lte("created_at", rangeEndIso) .order("created_at", { ascending: true }) .order("id", { ascending: true }) .limit(10000), supabase .from("users") .select("id, username") .limit(5000), ]); if (txRes.error) { loadError = txRes.error.message; } else { transactions = (txRes.data ?? []) as DbTransaction[]; } if (usersRes.error && !loadError) { loadError = usersRes.error.message; } else if (!usersRes.error) { userNameRows = (usersRes.data ?? []) as UsernameRow[]; } } const usernameById = new Map(); for (const u of userNameRows) { usernameById.set(u.id, u.username ?? null); } const book = transactions.length === 0 ? { pairs: [] as ReturnType["pairs"], totalDebitCoins: BigInt(0), totalCreditCoins: BigInt(0), balanced: true, } : buildLedgerBookPairs(transactions, usernameById); const matchAudits = auditMatchEconomics(transactions); const unbalancedMatches = matchAudits.filter((m) => !m.balanced); const rewardDeltaTotalCoins = matchAudits.reduce( (sum, m) => sum + m.rewardDeltaCoins, BigInt(0), ); return (

Ledger book

Double-entry presentation in{" "} RC{" "} (from on-chain coin units). Each transaction posts a debit then a matching credit; running balance returns to{" "} 0.0 {" "} after every pair. Period totals must match for a balanced book.

Reset to last month (UTC) ← Dashboard

Inclusive UTC range: {rangeStartIso.slice(0, 10)} →{" "} {rangeEndIso.slice(0, 10)} · {transactions.length.toLocaleString("en-US")}{" "} transactions loaded (cap 10,000).

{configError ? (
{configError}
) : loadError ? (

{loadError}

) : ( <>
Period balance: {book.balanced ? ( Total debits = total credits ( {formatRcLabelFromCoinsBigInt(book.totalDebitCoins)} in coin-derived RC) — balanced ✓ ) : ( Mismatch — debits{" "} {formatRcLabelFromCoinsBigInt(book.totalDebitCoins)} vs credits{" "} {formatRcLabelFromCoinsBigInt(book.totalCreditCoins)} )}
Match economics: {rewardDeltaTotalCoins === BigInt(0) && unbalancedMatches.length === 0 ? ( reward equals entry_hold for all matches — balanced ✓ ) : ( reward-entry_hold delta{" "} {rewardDeltaTotalCoins > BigInt(0) ? `+${rewardDeltaTotalCoins.toString()}` : rewardDeltaTotalCoins.toString()} {" "} coins across {unbalancedMatches.length} unbalanced match {unbalancedMatches.length === 1 ? "" : "es"} )}
{book.pairs.length === 0 ? ( ) : ( book.pairs.map((p, idx) => { const rcAmt = formatRcDecimalFromCoinsBigInt(p.amountCoins); const refCell = p.matchId != null ? ( {p.refLabel} · M{p.matchId} ) : ( {p.refLabel} ); return ( ); }) )} {book.pairs.length > 0 ? ( ) : null}
Date Particulars Folio / ref Debit (RC) Credit (RC) Balance (RC)
No transactions in this range.
{p.dateDisplay} {p.drParticulars} {refCell} {rcAmt} {formatSignedRcFromCoinsBigInt( p.balanceAfterDebitCoins, )}
{p.crParticulars} {rcAmt} {formatSignedRcFromCoinsBigInt( p.balanceAfterCreditCoins, )}
Period totals {formatRcDecimalFromCoinsBigInt(book.totalDebitCoins)} {formatRcDecimalFromCoinsBigInt(book.totalCreditCoins)} {formatSignedRcFromCoinsBigInt( book.totalDebitCoins - book.totalCreditCoins, )}
)}
); }