From df8f5bc9a9c4cfc3fd04421857012b6d1c428a3e Mon Sep 17 00:00:00 2001 From: voidarc Date: Tue, 7 Jul 2026 20:43:55 +0100 Subject: [PATCH] added registrations page --- src/routes/+layout.svelte | 89 +++++++-- src/routes/api/registrations/[id]/+server.ts | 69 +++++++ src/routes/registrations/+page.server.ts | 104 +++++++++++ src/routes/registrations/+page.svelte | 186 +++++++++++++++++++ 4 files changed, 433 insertions(+), 15 deletions(-) create mode 100644 src/routes/api/registrations/[id]/+server.ts create mode 100644 src/routes/registrations/+page.server.ts create mode 100644 src/routes/registrations/+page.svelte diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 30ad3a3..9c82231 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,32 @@ import type { LayoutData } from './$types'; let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props(); + + let adminOpen = $state(false); + let wrapperRef = $state(null); + + function toggleAdmin() { + adminOpen = !adminOpen; + } + + function closeAdmin() { + adminOpen = false; + } + + $effect(() => { + if (!adminOpen) return; + + const handler = (e: MouseEvent) => { + if (wrapperRef && !wrapperRef.contains(e.target as Node)) { + closeAdmin(); + } + }; + const timer = setTimeout(() => document.addEventListener('click', handler), 0); + return () => { + clearTimeout(timer); + document.removeEventListener('click', handler); + }; + }); @@ -17,21 +43,24 @@
{#if data.user?.role === 'admin'} - ledger - import - settings + {/if} {#if data.user} diff --git a/src/routes/api/registrations/[id]/+server.ts b/src/routes/api/registrations/[id]/+server.ts new file mode 100644 index 0000000..cb327ae --- /dev/null +++ b/src/routes/api/registrations/[id]/+server.ts @@ -0,0 +1,69 @@ +import { error, json } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import { db } from '$lib/server/db'; +import { registeredPlayers, registeredEvents, scorers } from '$lib/server/db/schema'; +import { globalEmitter } from '$lib/server/globalEmitter'; + +export async function PATCH({ params, request, locals }: any) { + if (!locals.user) throw error(401, 'Unauthorized'); + const [row] = await db.select({ role: scorers.role }).from(scorers).where(eq(scorers.id, locals.user.id)); + if (row?.role !== 'admin') throw error(403, 'Forbidden'); + + const id = parseInt(params.id); + if (isNaN(id)) throw error(400, 'Invalid registration ID'); + + const body = await request.json(); + if (typeof body.playerId !== 'number') throw error(400, 'playerId is required'); + + const reg = await db + .select({ + registeredEventID: registeredPlayers.registeredEventID + }) + .from(registeredPlayers) + .where(eq(registeredPlayers.id, id)) + .get(); + + if (!reg) throw error(404, 'Registration not found'); + + await db + .update(registeredPlayers) + .set({ playerID: body.playerId }) + .where(eq(registeredPlayers.id, id)); + + globalEmitter.emit('eventUpdate'); + return json({ success: true }); +} + +export async function DELETE({ params, locals }: any) { + if (!locals.user) throw error(401, 'Unauthorized'); + const [row] = await db.select({ role: scorers.role }).from(scorers).where(eq(scorers.id, locals.user.id)); + if (row?.role !== 'admin') throw error(403, 'Forbidden'); + + const id = parseInt(params.id); + if (isNaN(id)) throw error(400, 'Invalid registration ID'); + + const reg = await db + .select({ + registeredEventID: registeredPlayers.registeredEventID + }) + .from(registeredPlayers) + .where(eq(registeredPlayers.id, id)) + .get(); + + if (!reg) throw error(404, 'Registration not found'); + + const event = await db + .select({ state: registeredEvents.state }) + .from(registeredEvents) + .where(eq(registeredEvents.id, reg.registeredEventID)) + .get(); + + if (!event || event.state !== 0) { + throw error(403, 'Can only delete registrations from events that have not started'); + } + + await db.delete(registeredPlayers).where(eq(registeredPlayers.id, id)); + + globalEmitter.emit('eventUpdate'); + return json({ success: true }); +} diff --git a/src/routes/registrations/+page.server.ts b/src/routes/registrations/+page.server.ts new file mode 100644 index 0000000..7df2c33 --- /dev/null +++ b/src/routes/registrations/+page.server.ts @@ -0,0 +1,104 @@ +import { error, redirect } from '@sveltejs/kit'; +import { eq, sql } from 'drizzle-orm'; +import { db } from '$lib/server/db'; +import { + scorers, + registeredPlayers, + registeredEvents, + brackets, + eventTypes, + players, + teams, + divisions, + scoringPresets +} from '$lib/server/db/schema'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user) throw redirect(303, '/login'); + + const [row] = await db + .select({ role: scorers.role }) + .from(scorers) + .where(eq(scorers.id, locals.user.id)); + + if (row?.role !== 'admin') throw error(403, 'Forbidden'); + + const events = await db + .select({ + id: registeredEvents.id, + name: eventTypes.name, + division: divisions.name, + state: registeredEvents.state + }) + .from(registeredEvents) + .innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id)) + .innerJoin(divisions, eq(registeredEvents.division, divisions.id)) + .orderBy(registeredEvents.id); + + const registrations = await db + .select({ + registeredPlayerId: registeredPlayers.id, + playerId: players.id, + firstName: players.firstName, + lastName: players.lastName, + placement: registeredPlayers.placement, + bracket: brackets.name, + eventId: registeredEvents.id, + eventName: eventTypes.name, + teamId: teams.id, + teamName: teams.name, + teamColor: teams.color, + state: registeredEvents.state, + division: divisions.name, + points: sql`COALESCE(( + SELECT ${scoringPresets.points} + FROM ${scoringPresets} + WHERE ${scoringPresets.presetID} = ${eventTypes.scoringPreset} + AND ${scoringPresets.placement} = ${registeredPlayers.placement} + ), 0)` + .mapWith(Number) + .as('points') + }) + .from(registeredPlayers) + .innerJoin(registeredEvents, eq(registeredPlayers.registeredEventID, registeredEvents.id)) + .innerJoin(brackets, eq(registeredPlayers.bracket, brackets.id)) + .innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id)) + .innerJoin(players, eq(registeredPlayers.playerID, players.id)) + .innerJoin(teams, eq(players.team, teams.id)) + .innerJoin(divisions, eq(registeredEvents.division, divisions.id)) + .orderBy(registeredPlayers.id); + + const allPlayers = await db + .select({ + id: players.id, + firstName: players.firstName, + lastName: players.lastName, + teamId: players.team + }) + .from(players) + .orderBy(players.firstName); + + return { + events: events.map((e) => ({ id: e.id, name: e.name, division: e.division, state: e.state })), + registrations: registrations.map((r) => ({ + id: r.registeredPlayerId, + registeredPlayerId: r.registeredPlayerId, + playerId: r.playerId, + firstName: r.firstName, + lastName: r.lastName, + placement: r.placement, + bracket: r.bracket, + eventId: r.eventId, + eventName: r.eventName, + teamId: r.teamId, + teamName: r.teamName, + teamColor: r.teamColor, + state: r.state, + division: r.division, + points: r.points, + stateLabel: r.state === 0 ? 'Not started' : r.state === 1 ? 'Ongoing' : 'Finished' + })), + allPlayers + }; +}; diff --git a/src/routes/registrations/+page.svelte b/src/routes/registrations/+page.svelte new file mode 100644 index 0000000..1fa4a73 --- /dev/null +++ b/src/routes/registrations/+page.svelte @@ -0,0 +1,186 @@ + + +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + + + {#snippet header()} + + + + + + + + + + + {/snippet} + {#snippet row(reg: Registration)} + + + + + + + + + + + {/snippet} +
RP IDEventTeamDivisionBracketPlayer NameEvent StatePlacementPointsActions{reg.registeredPlayerId}{reg.eventName}{reg.teamName}{reg.division}{reg.bracket} + {#if editingId === reg.registeredPlayerId} + + {:else} + {reg.firstName} {reg.lastName} + {/if} + {reg.stateLabel}{reg.placement > 0 ? reg.placement : '-'}{reg.points > 0 ? reg.points : '-'} + {#if editingId === reg.registeredPlayerId} + + + {:else} + + {#if reg.state === 0} + + {/if} + {/if} +
+
+ +