From ec56e3794ec0330d3105e40976a604aba1c8485c Mon Sep 17 00:00:00 2001 From: voidarc Date: Sat, 4 Jul 2026 15:51:49 +0100 Subject: [PATCH] added ledger view (thanks opencode) --- scripts/seed.ts | 43 ++++------ src/lib/server/auth.ts | 2 +- src/lib/server/databaseManager.ts | 60 ++++++++++++- src/lib/ui/Table.svelte | 2 +- src/routes/api/ledger/+server.ts | 47 +++++++++++ src/routes/ledger/+page.server.ts | 9 +- src/routes/ledger/+page.svelte | 135 +++++++++++++++++++++++++++++- 7 files changed, 266 insertions(+), 32 deletions(-) create mode 100644 src/routes/api/ledger/+server.ts diff --git a/scripts/seed.ts b/scripts/seed.ts index 2041496..05d2028 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -155,34 +155,27 @@ async function seed() { const dbEventTypes = await db.select().from(schema.eventTypes); const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id])); - // Seed registered events + // Seed registered events (auto-generated from event types × divisions, ordered by division then event type) const eventNameMap = new Map(); - const registeredEventsCSV = readCSV('registeredEvents.csv'); - for (const row of registeredEventsCSV) { - const eventTypeId = eventTypeMap.get(row.event_type); - const teamId = teamMap.get(row.winner); - const divisionId = divisionMap.get(row.division); + for (const division of dbDivisions) { + for (const eventType of dbEventTypes) { + const [inserted] = await db + .insert(schema.registeredEvents) + .values({ + eventType: eventType.id, + division: division.id, + state: 0, + timeCompleted: null, + teamWinner: null + }) + .returning(); + console.log( + ` → Registered Event [id:${inserted.id}]: ${eventType.name} | ${division.name}` + ); - if (!eventTypeId) throw new Error(`Event Type "${row.event_type}" not found`); - if (!divisionId) throw new Error(`Division "${row.division}" not found`); - - const [inserted] = await db - .insert(schema.registeredEvents) - .values({ - eventType: eventTypeId, - division: divisionId, - state: row.event_state || 0, - timeCompleted: row.time_completed || null, - teamWinner: teamId || null - }) - .returning(); - console.log( - ` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}, winner: ${teamId}, ${row.winner}` - ); - - // Map event name|division to the generated event ID - eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id); + eventNameMap.set(`${eventType.name}|${division.name}`, inserted.id); + } } // Seed registered players (linking players to events) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index a6dc8e0..76f7c03 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -47,7 +47,7 @@ export async function validateSessionToken(token: string) { row.session.expiresAt = newExpiresAt; } - return { session: row.session, user: { id: row.user.id, username: row.user.username } }; + return { session: row.session, user: { id: row.user.id, username: row.user.username, role: row.user.role } }; } export async function invalidateSession(sessionId: string) { diff --git a/src/lib/server/databaseManager.ts b/src/lib/server/databaseManager.ts index 75e05f8..da9205b 100644 --- a/src/lib/server/databaseManager.ts +++ b/src/lib/server/databaseManager.ts @@ -1,5 +1,5 @@ import { db } from '$lib/server/db'; -import { sql, eq, and } from 'drizzle-orm'; +import { sql, eq, and, desc, inArray } from 'drizzle-orm'; import * as schema from '$lib/server/db/schema'; import { globalEmitter } from './globalEmitter'; @@ -214,3 +214,61 @@ export async function getRegisteredEventsWithPlayers(eventId?: number) { } return fullEventList; } + +export async function getLedgerEntries(limit: number | 'all' = 100) { + let query = db + .select({ + id: schema.mainLedger.id, + timestamp: schema.mainLedger.timestamp, + updateType: schema.mainLedger.updateType, + eventName: schema.eventTypes.name + }) + .from(schema.mainLedger) + .leftJoin( + schema.registeredEvents, + eq(schema.mainLedger.registeredEvent, schema.registeredEvents.id) + ) + .leftJoin( + schema.eventTypes, + eq(schema.registeredEvents.eventType, schema.eventTypes.id) + ) + .orderBy(desc(schema.mainLedger.timestamp)) as any; + + if (limit !== 'all') { + query = query.limit(limit); + } + + const entries = await query; + + if (entries.length === 0) return []; + + const ids = entries.map((e: any) => e.id); + + const scores = await db + .select({ + ledgerID: schema.scoreLedger.ledgerID, + teamName: schema.teams.name, + points: schema.scoreLedger.points + }) + .from(schema.scoreLedger) + .leftJoin(schema.teams, eq(schema.scoreLedger.teamID, schema.teams.id)) + .where(inArray(schema.scoreLedger.ledgerID, ids)); + + const scoresByLedger = new Map>(); + for (const s of scores) { + if (s.ledgerID == null || s.teamName == null) continue; + if (!scoresByLedger.has(s.ledgerID)) { + scoresByLedger.set(s.ledgerID, {}); + } + const map = scoresByLedger.get(s.ledgerID)!; + map[s.teamName] = (map[s.teamName] || 0) + s.points; + } + + return entries.map((e: any) => ({ + id: e.id, + type: e.updateType, + eventName: e.eventName || '', + timestamp: e.timestamp, + teamPoints: scoresByLedger.get(e.id) || {} + })); +} diff --git a/src/lib/ui/Table.svelte b/src/lib/ui/Table.svelte index 3c9ed0c..5672d0f 100644 --- a/src/lib/ui/Table.svelte +++ b/src/lib/ui/Table.svelte @@ -23,6 +23,7 @@ const rowOffsetTop = targetRow.offsetTop; const rowHeight = targetRow.offsetHeight; + const containerHeight = containerRef.offsetHeight; const centerScrollTarget = rowOffsetTop - containerHeight / 2 + rowHeight / 2; @@ -32,7 +33,6 @@ }); } } - }
diff --git a/src/routes/api/ledger/+server.ts b/src/routes/api/ledger/+server.ts new file mode 100644 index 0000000..132c38b --- /dev/null +++ b/src/routes/api/ledger/+server.ts @@ -0,0 +1,47 @@ +import { getLedgerEntries } from '$lib/server/databaseManager'; +import { globalEmitter } from '$lib/server/globalEmitter'; +import { db } from '$lib/server/db'; +import * as schema from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; + +export async function GET({ url }: any) { + const limit = url.searchParams.get('limit') || '100'; + const entries = await getLedgerEntries(limit === 'all' ? 'all' : parseInt(limit)); + return new Response(JSON.stringify(entries), { + headers: { 'Content-Type': 'application/json' } + }); +} + +export async function POST({ request, locals }: any) { + if (!locals.user || locals.user.role !== 'admin') { + return new Response('Unauthorized', { status: 401 }); + } + + const body = await request.json(); + const { eventId, teamPoints: teamPointsByName } = body; + + const teams = await db.select().from(schema.teams); + const teamNameToId = new Map(teams.map((t) => [t.name, t.id])); + + const newLedgerEntry = await db + .insert(schema.mainLedger) + .values({ registeredEvent: eventId, updateType: 'admin' }) + .returning(); + + const ledgerEntryId = newLedgerEntry[0].id; + + const scoreEntries: { ledgerID: number; teamID: number; points: number }[] = []; + for (const [name, points] of Object.entries(teamPointsByName as Record)) { + if (points === 0) continue; + const teamID = teamNameToId.get(name); + if (teamID === undefined) continue; + scoreEntries.push({ ledgerID: ledgerEntryId, teamID, points }); + } + + if (scoreEntries.length > 0) { + await db.insert(schema.scoreLedger).values(scoreEntries); + } + + globalEmitter.emit('scoreUpdate'); + return new Response('ok'); +} diff --git a/src/routes/ledger/+page.server.ts b/src/routes/ledger/+page.server.ts index 512e712..f0e3098 100644 --- a/src/routes/ledger/+page.server.ts +++ b/src/routes/ledger/+page.server.ts @@ -1,8 +1,8 @@ -// src/routes/admin/+page.server.ts import { error, redirect } from '@sveltejs/kit'; import { eq } from 'drizzle-orm'; import { db } from '$lib/server/db'; -import { scorers } from '$lib/server/db/schema'; +import { scorers, teams } from '$lib/server/db/schema'; +import { getRegisteredEvents } from '$lib/server/databaseManager'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals }) => { @@ -19,5 +19,8 @@ export const load: PageServerLoad = async ({ locals }) => { throw error(403, 'Forbidden'); } - return { user: locals.user }; + const allTeams = await db.select({ id: teams.id, name: teams.name }).from(teams).orderBy(teams.name); + const events = await getRegisteredEvents(); + + return { teams: allTeams, events: events.events.map((e) => ({ id: e.id, name: e.name, division: e.division })) }; }; diff --git a/src/routes/ledger/+page.svelte b/src/routes/ledger/+page.svelte index 876360d..7ddcbf6 100644 --- a/src/routes/ledger/+page.svelte +++ b/src/routes/ledger/+page.svelte @@ -1 +1,134 @@ -
some
+ + +
+

Manual Ledger Entry

+
+
+ + +
+ {#each teamNames as team} +
+ + { inputPoints[team] = (e.target as HTMLInputElement).value; }} + class="border rounded px-2 py-1 w-20" + /> +
+ {/each} + +
+ + + {#snippet header()} + + + + {#each teamNames as team} + + {/each} + + {/snippet} + {#snippet row(entry: LedgerEntry)} + + + + {#each teamNames as team} + + {/each} + + {/snippet} +
IDTypeEvent{team}Time{entry.id}{entry.type}{entry.eventName} + {entry.teamPoints[team] !== undefined ? formatPoints(entry.teamPoints[team]) : ''} + {formatTime(entry.timestamp)}
+ +
+ + {entries.length} entries shown +
+