added ledger view (thanks opencode)

This commit is contained in:
2026-07-04 15:51:49 +01:00
parent bfa49cfa91
commit ec56e3794e
7 changed files with 266 additions and 32 deletions

View File

@@ -1 +1,134 @@
<div>some</div>
<script lang="ts">
import Table from '$lib/ui/Table.svelte';
import type { PageData } from './$types';
type LedgerEntry = {
id: number;
type: string;
eventName: string;
timestamp: string;
teamPoints: Record<string, number>;
};
let { data }: { data: PageData } = $props();
let entries = $state<LedgerEntry[]>([]);
let showAll = $state(false);
let selectedEventId = $state<number | ''>('');
let inputPoints = $state<Record<string, string>>({});
let submitting = $state(false);
let teamNames = $derived(data.teams.map((t) => t.name).sort());
function formatTime(ts: string) {
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
}
function formatPoints(n: number) {
return n >= 0 ? `+${n}` : `${n}`;
}
async function loadEntries(all: boolean = false) {
const res = await fetch(`/api/ledger?limit=${all ? 'all' : '100'}`);
entries = await res.json();
showAll = all;
}
async function submitEntry() {
const event = data.events.find((e) => e.id === selectedEventId);
if (!event) return;
submitting = true;
try {
const teamPoints: Record<string, number> = {};
for (const [team, val] of Object.entries(inputPoints)) {
const num = parseInt(val);
if (!isNaN(num) && num !== 0) {
teamPoints[team] = num;
}
}
await fetch('/api/ledger', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ eventId: event.id, teamPoints })
});
selectedEventId = '';
inputPoints = {};
await loadEntries(showAll);
} finally {
submitting = false;
}
}
$effect(() => {
loadEntries();
});
</script>
<div class="p-4">
<h2 class="mb-4 text-lg font-bold">Manual Ledger Entry</h2>
<div class="mb-4 flex flex-wrap gap-4 items-end">
<div>
<label class="block text-sm mb-1">Event</label>
<select bind:value={selectedEventId} class="border rounded px-2 py-1">
<option value="">Select event...</option>
{#each data.events as event (event.id)}
<option value={event.id}>{event.name} {event.division}</option>
{/each}
</select>
</div>
{#each teamNames as team}
<div>
<label class="block text-sm mb-1">{team}</label>
<input
type="number"
value={inputPoints[team] ?? '0'}
oninput={(e) => { inputPoints[team] = (e.target as HTMLInputElement).value; }}
class="border rounded px-2 py-1 w-20"
/>
</div>
{/each}
<button
onclick={submitEntry}
disabled={!selectedEventId || submitting}
class="border rounded px-4 py-1 bg-green-700 text-white disabled:opacity-50"
>
Submit
</button>
</div>
<Table data={entries} maxHeight="none">
{#snippet header()}
<th class="px-2">ID</th>
<th class="px-2">Type</th>
<th class="px-2">Event</th>
{#each teamNames as team}
<th class="px-2">{team}</th>
{/each}
<th class="px-2">Time</th>
{/snippet}
{#snippet row(entry: LedgerEntry)}
<td class="px-2">{entry.id}</td>
<td class="px-2">{entry.type}</td>
<td class="px-2">{entry.eventName}</td>
{#each teamNames as team}
<td class="px-2">
{entry.teamPoints[team] !== undefined ? formatPoints(entry.teamPoints[team]) : ''}
</td>
{/each}
<td class="px-2">{formatTime(entry.timestamp)}</td>
{/snippet}
</Table>
<div class="mt-4">
<button
onclick={() => loadEntries(!showAll)}
class="border rounded px-4 py-1"
>
{showAll ? 'Show 100 most recent' : 'Load all entries'}
</button>
<span class="ml-2 text-sm">{entries.length} entries shown</span>
</div>
</div>