135 lines
3.5 KiB
Svelte
135 lines
3.5 KiB
Svelte
<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>
|