add supply
This commit is contained in:
@@ -215,6 +215,8 @@ type Props = {
|
||||
matchmakerSource: MatchmakerLogSource;
|
||||
matchmakerContent: string;
|
||||
matchmakerError: string | null;
|
||||
/** Ledger: add-system-supply action failed (URL `supplyErr=1`). */
|
||||
supplyError: boolean;
|
||||
};
|
||||
|
||||
function StatCard({
|
||||
@@ -264,6 +266,7 @@ export function AdminDashboard({
|
||||
matchmakerSource,
|
||||
matchmakerContent,
|
||||
matchmakerError,
|
||||
supplyError,
|
||||
}: Props) {
|
||||
const [hideNoWinner, setHideNoWinner] = useState(true);
|
||||
const [lbSortKey, setLbSortKey] = useState<LeaderboardSortKey>("wins");
|
||||
@@ -735,6 +738,7 @@ export function AdminDashboard({
|
||||
participantRaw={participantRaw}
|
||||
ledgerFrom={ledgerFrom}
|
||||
ledgerTo={ledgerTo}
|
||||
supplyError={supplyError}
|
||||
/>
|
||||
) : (
|
||||
<section className="space-y-3">
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ClickableUserId } from "@/components/clickable-user-id";
|
||||
import { buildDashboardHref } from "@/lib/dashboard-search-url";
|
||||
import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc";
|
||||
import {
|
||||
formatRcBalanceWithCoins,
|
||||
formatRcLabelFromCoinsBigInt,
|
||||
} from "@/lib/coins-rc";
|
||||
import { auditMatchEconomics } from "@/lib/ledger-match-audit";
|
||||
import {
|
||||
computeLedgerIntegrity,
|
||||
@@ -21,6 +24,7 @@ import {
|
||||
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";
|
||||
|
||||
@@ -86,6 +90,7 @@ type Props = {
|
||||
participantRaw: string | null;
|
||||
ledgerFrom: string;
|
||||
ledgerTo: string;
|
||||
supplyError: boolean;
|
||||
};
|
||||
|
||||
export function AdminLedger({
|
||||
@@ -104,6 +109,7 @@ export function AdminLedger({
|
||||
participantRaw,
|
||||
ledgerFrom,
|
||||
ledgerTo,
|
||||
supplyError,
|
||||
}: Props) {
|
||||
const usernameById = useMemo(() => {
|
||||
const m = new Map<number, string | null>();
|
||||
@@ -113,6 +119,13 @@ export function AdminLedger({
|
||||
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 {
|
||||
@@ -240,6 +253,25 @@ export function AdminLedger({
|
||||
);
|
||||
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";
|
||||
@@ -275,12 +307,23 @@ export function AdminLedger({
|
||||
: "border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/40",
|
||||
].join(" ")}
|
||||
>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
|
||||
Σ user nets vs net issued
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
All transactions
|
||||
</p>
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
|
||||
Σ user nets vs net issued
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
All transactions
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSupplyModalOpen(true)}
|
||||
className="shrink-0 rounded-md border border-zinc-300 bg-white/90 px-2.5 py-1 text-xs font-medium text-zinc-800 shadow-sm hover:bg-white dark:border-zinc-600 dark:bg-zinc-900/90 dark:text-zinc-100 dark:hover:bg-zinc-900"
|
||||
>
|
||||
Add supply
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-sm tabular-nums text-zinc-900 dark:text-zinc-100">
|
||||
<p className="font-mono">
|
||||
{formatBigAmount(integrityGlobal.sumUserNets)}
|
||||
@@ -956,6 +999,122 @@ export function AdminLedger({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{supplyModalOpen ? (
|
||||
<div
|
||||
className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/40 p-4"
|
||||
role="presentation"
|
||||
onClick={() => setSupplyModalOpen(false)}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="supply-modal-title"
|
||||
className="w-full max-w-md rounded-xl border border-zinc-200 bg-white p-6 shadow-lg dark:border-zinc-700 dark:bg-zinc-900"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2
|
||||
id="supply-modal-title"
|
||||
className="text-lg font-semibold text-zinc-900 dark:text-zinc-50"
|
||||
>
|
||||
Add system supply
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
Credits user id{" "}
|
||||
<span className="font-mono">1</span>
|
||||
{systemAccount?.username ? (
|
||||
<span>
|
||||
{" "}
|
||||
(<span className="font-mono">{systemAccount.username}</span>)
|
||||
</span>
|
||||
) : null}
|
||||
: mint row (<span className="font-mono">to = 1</span>,{" "}
|
||||
<span className="font-mono">remarks = supply</span>), RC increases
|
||||
by the same coin amount.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
||||
Current RC:{" "}
|
||||
<span className="font-mono text-zinc-800 dark:text-zinc-200">
|
||||
{systemAccount
|
||||
? formatRcBalanceWithCoins(systemAccount.rc)
|
||||
: "— (user 1 not in loaded set)"}
|
||||
</span>
|
||||
</p>
|
||||
{supplyError ? (
|
||||
<p
|
||||
className="mt-3 text-sm text-red-600 dark:text-red-400"
|
||||
role="alert"
|
||||
>
|
||||
Could not add supply. Use a positive whole number of coin units
|
||||
(no decimals), and ensure the database and service key are
|
||||
available.
|
||||
</p>
|
||||
) : null}
|
||||
<form
|
||||
action={addSystemSupply}
|
||||
className="mt-5 space-y-4"
|
||||
>
|
||||
<input type="hidden" name="tab" value="ledger" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="highlightId"
|
||||
value={highlightId ?? ""}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="participantRaw"
|
||||
value={participantRaw ?? ""}
|
||||
/>
|
||||
<input type="hidden" name="ledgerFrom" value={ledgerFrom} />
|
||||
<input type="hidden" name="ledgerTo" value={ledgerTo} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="ledgerPage"
|
||||
value={String(ledgerPage)}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="ledgerPageSize"
|
||||
value={String(ledgerPageSize)}
|
||||
/>
|
||||
<input type="hidden" name="ledgerSort" value={ledgerSort} />
|
||||
<input type="hidden" name="ledgerOrder" value={ledgerOrder} />
|
||||
<div>
|
||||
<label
|
||||
htmlFor="supply-amount-modal"
|
||||
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300"
|
||||
>
|
||||
Coin amount
|
||||
</label>
|
||||
<input
|
||||
id="supply-amount-modal"
|
||||
name="supplyAmount"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
placeholder="e.g. 4000"
|
||||
className="mt-1 w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm tabular-nums text-zinc-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSupplyModalOpen(false)}
|
||||
className="rounded-md border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-800 shadow-sm hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md border border-emerald-600 bg-emerald-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 dark:border-emerald-700 dark:bg-emerald-800 dark:hover:bg-emerald-700"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user