Add Bank accounts, transactions, and pending transfer views
CI / checks (push) Successful in 7m50s

This commit is contained in:
2026-10-13 11:50:26 -04:00
parent d6147077d7
commit e85dc58130
19 changed files with 1888 additions and 7 deletions
@@ -0,0 +1,55 @@
<script lang="ts">
import { bankPageSize } from '$lib/bank.js';
import Button from '../primitives/Button.svelte';
let {
total,
offset,
disabled = false,
onchange,
label = 'results'
}: {
total: number;
offset: number;
disabled?: boolean;
onchange: (offset: number) => void;
label?: string;
} = $props();
</script>
<nav class="pagination" aria-label={`${label} pagination`}>
<span role="status"
>{total ? offset + 1 : 0}–{Math.min(offset + bankPageSize, total)} of {total.toLocaleString()}
{label}</span
>
{#if total > bankPageSize}
<div>
<Button
size="sm"
disabled={disabled || offset === 0}
onclick={() => onchange(offset - bankPageSize)}>Previous</Button
>
<Button
size="sm"
disabled={disabled || offset + bankPageSize >= total}
onclick={() => onchange(offset + bankPageSize)}>Next</Button
>
</div>
{/if}
</nav>
<style>
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-3);
padding-block: var(--space-3);
color: var(--text-secondary);
font-size: var(--text-sm);
}
.pagination div {
display: flex;
gap: var(--space-2);
}
</style>
@@ -0,0 +1,53 @@
<script lang="ts">
import type { BankPending } from '$lib/bank.js';
import { contextHref } from '$lib/navigation-context.js';
import CopyValue from '../CopyValue.svelte';
import RelativeTime from '../RelativeTime.svelte';
import ServerIdentity from '../ServerIdentity.svelte';
let { transfer, source }: { transfer: BankPending; source: URL } = $props();
</script>
<div class="pending-details">
<p>
Waiting for the game server to confirm or cancel this {transfer.operation === 'deposit'
? 'deposit'
: 'withdrawal'}.
</p>
<p>
{transfer.operation === 'deposit'
? 'These points have not been added to the bank balance. Deposit capacity is reserved until the transfer settles.'
: 'These points are reserved and cannot be withdrawn again until the transfer settles.'}
</p>
<p>
Restore the server’s connection and allow it to retry. Pending transfers do not expire
automatically.
</p>
<div>
<ServerIdentity name={transfer.server} /> · <RelativeTime value={transfer.createdAt} />
</div>
{#if transfer.matchAvailable}<a href={contextHref(`/matches/${transfer.matchId}`, source)}
>View match #{transfer.matchId}</a
>{:else if transfer.matchId}<span>Match #{transfer.matchId} · History expired</span>{/if}
<details>
<summary>Transfer details</summary><CopyValue
value={transfer.requestId}
label="Request ID"
/><CopyValue value={transfer.server} label="Server ID" />
</details>
</div>
<style>
.pending-details {
display: grid;
gap: var(--space-3);
font-size: var(--text-sm);
color: var(--text-secondary);
}
p {
margin: 0;
}
summary {
padding-block: var(--space-2);
color: var(--text-primary);
}
</style>
@@ -0,0 +1,70 @@
<script lang="ts">
import type { BankTransfer } from '$lib/bank.js';
import { contextHref } from '$lib/navigation-context.js';
import RelativeTime from '../RelativeTime.svelte';
import ServerIdentity from '../ServerIdentity.svelte';
import EmptyState from '../primitives/EmptyState.svelte';
let {
entries,
source,
onPlayer,
compact = false
}: {
entries: BankTransfer[];
source: URL;
onPlayer?: (guid: string) => void;
compact?: boolean;
} = $props();
const number = new Intl.NumberFormat('en-US');
</script>
{#if entries.length}
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard users need to scroll wide tables.) -->
<div class="bank-table-scroll" role="region" aria-label="Bank transactions" tabindex="0">
<table class="bank-table" class:compact>
<caption class="sr-only">Completed bank transfers</caption>
<thead
><tr
><th scope="col">Time</th>{#if onPlayer}<th scope="col">Player</th>{/if}<th
scope="col">Transfer</th
><th scope="col">Balance after</th><th scope="col">Server / match</th></tr
></thead
>
<tbody>
{#each entries as entry (entry.id)}
<tr>
<td><RelativeTime value={entry.createdAt} /></td>
{#if onPlayer}<td
><button class="bank-player" onclick={() => onPlayer?.(entry.guid)}
>{entry.name ?? entry.guid}</button
><small class="bank-id">{entry.guid}</small></td
>{/if}
<td class="bank-number"
><strong
>{entry.amount > 0 ? '+' : '−'}{number.format(
Math.abs(entry.amount)
)}</strong
><small>{entry.amount > 0 ? 'Deposit' : 'Withdrawal'}</small></td
>
<td class="bank-number">{number.format(entry.balance)}</td>
<td
><ServerIdentity name={entry.server} /><small
>{#if entry.matchAvailable}<a
href={contextHref(`/matches/${entry.matchId}`, source)}
>Match #{entry.matchId}</a
>{:else if entry.matchId}Match #{entry.matchId} · History expired{:else}Match
unavailable{/if}</small
></td
>
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
<EmptyState
title="No transactions found"
description="Completed deposits and withdrawals appear here. Try changing your filters if you expected to see more."
/>
{/if}
+68
View File
@@ -0,0 +1,68 @@
.bank-table-scroll {
max-width: 100%;
overflow-x: auto;
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
background: var(--surface);
}
.bank-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: var(--text-sm);
}
.bank-table th {
color: var(--text-tertiary);
background: var(--surface-sunken);
font-weight: var(--weight-medium);
white-space: nowrap;
}
.bank-table th,
.bank-table td {
padding: var(--space-4);
vertical-align: top;
}
.bank-table tbody tr + tr {
border-top: 1px solid var(--border-subtle);
}
.bank-table td {
min-width: 130px;
}
.bank-table small {
display: block;
margin-top: var(--space-1);
font-size: var(--text-xs);
color: var(--text-secondary);
}
.bank-table .bank-id {
font-family: var(--font-mono);
overflow-wrap: anywhere;
}
.bank-number {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.bank-player {
padding: 0;
background: none;
border: 0;
color: var(--accent-primary-hover);
text-align: left;
font: inherit;
font-weight: var(--weight-semibold);
overflow-wrap: anywhere;
cursor: pointer;
}
.bank-player:hover {
text-decoration: underline;
}
.bank-table.compact th,
.bank-table.compact td {
padding: var(--space-3);
}
.bank-table.compact td {
min-width: 100px;
}
.bank-table .bank-warning {
color: var(--state-warning);
}