From f773c52b0acfc235da3e4ee0009facb373e33e2b Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Mon, 7 Sep 2026 23:57:45 +0200 Subject: [PATCH] feat: mehrere Teilnehmer auf einmal eintragen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ein Event beginnt mit zwanzig Anmeldungen, nicht mit einer. Bisher hiess das: zwanzigmal denselben Dialog oeffnen, einen Namen aus einer Auswahlliste suchen, eine Nummer tippen, speichern. Jetzt steht der Kader als Liste da — Haken und Startnummer nebeneinander, darueber eine Suche und "Alle"/"Keinen". Der Knopf zaehlt mit, was ausgewaehlt ist. Geprueft wird vor dem Schreiben, nicht dabei: doppelte Nummern innerhalb der Auswahl und gegen die schon vergebenen. Eine Kollision soll nicht erst auffallen, wenn die halbe Liste eingetragen ist. Der alte Dialog bleibt, aber nur noch fuers Aendern einer Startnummer — sein Anlege-Zweig samt Fahrer-Auswahlliste ist entfallen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr --- .../routes/dashboard/events/[id]/+page.svelte | 228 ++++++++++++++---- 1 file changed, 180 insertions(+), 48 deletions(-) diff --git a/frontend/src/routes/dashboard/events/[id]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/+page.svelte index ea61c72..716bff2 100644 --- a/frontend/src/routes/dashboard/events/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/+page.svelte @@ -13,7 +13,6 @@ } from '$lib/stores/eventParticipants.svelte' import { auth } from '$lib/stores/pocketbase.svelte' import TimeDialog from '$lib/components/TimeDialog.svelte' - import * as Select from '@/components/ui/select' import { formatDateRange } from '$lib/time' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' @@ -34,6 +33,7 @@ Users, UserPlus, Check, + Search, X, } from 'lucide-svelte' import type { EventParticipantsResponse, StagesResponse } from '$lib/types' @@ -81,48 +81,110 @@ // --- Teilnehmer ------------------------------------------------------- let riderDialog = $state(false) let editing = $state(null) - let form = $state({ rider: '', number: '' }) + let form = $state({ number: '' }) let formError = $state(null) const available = $derived(event ? participants.available(event.id) : []) + // --- Mehrere auf einmal eintragen ------------------------------------- + // + // Ein Event beginnt mit zwanzig Anmeldungen, nicht mit einer. Zwanzigmal + // denselben Dialog zu oeffnen, einen Namen aus einer Liste zu suchen und + // eine Nummer zu tippen, ist dieselbe Arbeit zwanzigmal — hier steht der + // Kader stattdessen als Liste da, mit der Nummer gleich daneben. + let addDialog = $state(false) + let addQuery = $state('') + let addBusy = $state(false) + let addError = $state(null) + + /** Auswahl und Startnummer je Fahrer, nach Fahrer-ID. */ + let picks = $state>({}) + + const addFiltered = $derived.by(() => { + const q = addQuery.toLowerCase().trim() + return q ? available.filter((r) => riderName(r).toLowerCase().includes(q)) : available + }) + + const picked = $derived(available.filter((r) => picks[r.id]?.chosen)) + function openAdd() { - editing = null - form = { rider: available[0]?.id ?? '', number: '' } - formError = null - riderDialog = true + picks = Object.fromEntries(available.map((r) => [r.id, { chosen: false, number: '' }])) + addQuery = '' + addError = null + addDialog = true + } + + function toggleAll(on: boolean) { + const next = { ...picks } + for (const r of addFiltered) { + next[r.id] = { ...(next[r.id] ?? { number: '' }), chosen: on } + } + picks = next + } + + async function saveParticipants() { + if (!event || addBusy) return + + const chosen = picked + if (chosen.length === 0) { + addError = 'Niemand ausgewählt.' + return + } + + // Erst pruefen, dann schreiben: Eine doppelte Nummer soll nicht erst + // auffallen, wenn die halbe Liste schon eingetragen ist. + const seen = new Map() + for (const r of chosen) { + const number = (picks[r.id]?.number ?? '').trim() + if (!number) continue + + if (seen.has(number)) { + addError = `Startnummer ${number} ist doppelt vergeben (${seen.get(number)} und ${riderName(r)}).` + return + } + if (participants.isNumberTaken(event.id, number)) { + addError = `Startnummer ${number} ist in diesem Event bereits vergeben.` + return + } + seen.set(number, riderName(r)) + } + + addBusy = true + addError = null + try { + for (const r of chosen) { + await participants.add({ + event: event.id, + rider: r.id, + number: (picks[r.id]?.number ?? '').trim(), + }) + } + addDialog = false + } catch (e: any) { + addError = e.message ?? 'Nicht alle Teilnehmer konnten eingetragen werden.' + } finally { + addBusy = false + } } function openEdit(p: EventParticipantsResponse) { editing = p - form = { rider: p.rider, number: p.number ?? '' } + form = { number: p.number ?? '' } formError = null riderDialog = true } async function saveParticipant() { - if (!event) return + if (!event || !editing) return - if (!form.rider) { - formError = 'Bitte einen Fahrer wählen.' - return - } - if (participants.isNumberTaken(event.id, form.number, editing?.id)) { + if (participants.isNumberTaken(event.id, form.number, editing.id)) { formError = `Startnummer ${form.number} ist in diesem Event bereits vergeben.` return } formError = null try { - if (editing) { - await participants.edit(editing.id, { number: form.number.trim() }) - } else { - await participants.add({ - event: event.id, - rider: form.rider, - number: form.number.trim(), - }) - } + await participants.edit(editing.id, { number: form.number.trim() }) riderDialog = false } catch (e: any) { formError = e.message ?? 'Der Teilnehmer konnte nicht gespeichert werden.' @@ -489,36 +551,13 @@ - {editing ? 'Startnummer ändern' : 'Teilnehmer hinzufügen'} + Startnummer ändern - {#if editing} - Die Startnummer gilt nur für „{event?.name}". - {:else} - Aus dem Kader des Teams. Neue Fahrer legt die Teamleitung in den - Einstellungen an. - {/if} + Die Startnummer gilt nur für „{event?.name}".
- {#if !editing} -
- - - - {form.rider - ? riderName(riders.getById(form.rider) ?? ({} as any)) || 'Fahrer wählen…' - : 'Fahrer wählen…'} - - - {#each available as r (r.id)} - {riderName(r) || '— ohne Name —'} - {/each} - - -
- {/if} -
@@ -534,7 +573,100 @@ - + + + + + + + + + + Teilnehmer eintragen + + Aus dem Kader des Teams. Die Startnummer gilt je Event und kann leer + bleiben. + + + +
+ {#if available.length === 0} +

+ Der ganze Kader ist schon eingetragen. +

+ {:else} +
+
+ + +
+ + +
+ +
+ {#each addFiltered as r (r.id)} +
+ + (picks = { + ...picks, + [r.id]: { + number: picks[r.id]?.number ?? '', + chosen: e.currentTarget.checked, + }, + })} + /> + + + (picks = { + ...picks, + [r.id]: { + chosen: picks[r.id]?.chosen ?? false, + number: e.currentTarget.value, + }, + })} + /> +
+ {:else} +

+ Kein Treffer. +

+ {/each} +
+ {/if} + + {#if addError} +

{addError}

+ {/if} +
+ + + +