diff --git a/frontend/src/routes/dashboard/events/[id]/stages/[stageId]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/stages/[stageId]/+page.svelte index 4a51a8e..8443f0d 100644 --- a/frontend/src/routes/dashboard/events/[id]/stages/[stageId]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/stages/[stageId]/+page.svelte @@ -12,6 +12,7 @@ import { riderName, getRiderContext } from '$lib/stores/riders.svelte' import { getTimeContext } from '$lib/stores/times.svelte' import { getEventParticipantContext } from '$lib/stores/eventParticipants.svelte' + import { getTeamContext } from '$lib/stores/teams.svelte' import { ticker } from '$lib/stores/ticker.svelte' import { TIME_STATUS_LABEL } from '$lib/time' import { clockSync } from '$lib/stores/clockSync.svelte' @@ -21,13 +22,25 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import * as Table from '@/components/ui/table' - import { ArrowLeft, Clock, Download, Flag, Timer, Trophy, TriangleAlert } from 'lucide-svelte' + import { ArrowLeft, Clock, Download, Flag, Pencil, Timer, Trophy, TriangleAlert } from 'lucide-svelte' + import * as Dialog from '@/components/ui/dialog' + import { Input } from '@/components/ui/input' + import { Label } from '@/components/ui/label' const events = getEventContext() const stages = getStageContext() const riders = getRiderContext() const times = getTimeContext() const participants = getEventParticipantContext() + const teams = getTeamContext() + + // Die Namen der Teammitglieder holen: Ein Team kennt nur die IDs seiner + // Leute, und ohne diesen Zug stuende an jeder Zeit statt "Start: Anna" + // eine abgeschnittene ID. + $effect(() => { + void teams.active + teams.loadMembers() + }) const clock = ticker() @@ -43,6 +56,8 @@ const running = $derived(valid && stage ? times.runningByStage(stage.id, clock.now) : []) const leaderboard = $derived(valid && stage ? times.getLeaderboard(stage.id) : []) const unranked = $derived(valid && stage ? times.unrankedByStage(stage.id) : []) + /** Alle gewerteten Zeiten dieser Stage — Ziel einer Strafe für alle. */ + const stageTimes = $derived(leaderboard) /** * Laufende und gewertete Zeiten in einer Liste: Wer gerade unterwegs ist, @@ -68,6 +83,54 @@ let timeDialog = $state(false) let clockDialog = $state(false) + // --- Strafzeiten ------------------------------------------------------ + // + // Die Korrektur steckt als Sekunden an der Zeit (times.correction) und + // wird auf die Dauer gerechnet. Zwei Wege dorthin: an einer Zeit einzeln, + // das ist der Normalfall, und über alle Zeiten dieser Stage auf einmal — + // etwa wenn eine Schikane für alle nicht zählte. + let penaltyDialog = $state(false) + let penaltyTime = $state(null) + let penaltySeconds = $state(0) + let penaltyBusy = $state(false) + let penaltyError = $state(null) + + function openPenalty(timeId: string | null, current = 0) { + penaltyTime = timeId + penaltySeconds = current + penaltyError = null + penaltyDialog = true + } + + async function savePenalty() { + if (penaltyBusy) return + + penaltyBusy = true + penaltyError = null + try { + if (penaltyTime) { + await times.applyCorrection(penaltyTime, penaltySeconds) + } else { + // Auf die ganze Stage: Die Korrektur wird gesetzt, nicht + // addiert. Zweimal denselben Dialog abzuschicken darf keine + // doppelte Strafe ergeben. + for (const t of stageTimes) { + await times.applyCorrection(t.id, penaltySeconds) + } + } + penaltyDialog = false + } catch (e: any) { + penaltyError = e.message ?? 'Die Korrektur konnte nicht gespeichert werden.' + } finally { + penaltyBusy = false + } + } + + /** Wer hat gedrückt? Der Kontoname, sonst eine gekürzte ID. */ + function userLabel(id: string | undefined) { + return id ? teams.userLabel(id) : null + } + // --- Export ------------------------------------------------------------ function exportCsv() { if (leaderboard.length === 0) return @@ -174,10 +237,18 @@ - - - Zeiten ({leaderboard.length} gewertet{running.length ? `, ${running.length} laufend` : ''}) - +
+ + + Zeiten ({leaderboard.length} gewertet{running.length ? `, ${running.length} laufend` : ''}) + + {#if leaderboard.length > 0} + + {/if} +
{#if rows.length === 0} @@ -221,12 +292,44 @@ {:else} — {/if} + + + {#if userLabel(t.startedBy) || userLabel(t.stoppedBy)} + + {#if t.startedBy && t.stoppedBy && t.startedBy === t.stoppedBy} + Start und Stopp: {userLabel(t.startedBy)} + {:else} + {#if t.startedBy}Start: {userLabel(t.startedBy)}{/if} + {#if t.startedBy && t.stoppedBy} · {/if} + {#if t.stoppedBy}Stopp: {userLabel(t.stoppedBy)}{/if} + {/if} + + {/if} {t.formattedTime} - - {t.correction ? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)}s` : '–'} + + {/each} @@ -273,3 +376,45 @@ + + + + + + + {penaltyTime ? 'Zeitkorrektur' : 'Strafzeit für alle'} + + + {#if penaltyTime} + Wird auf die gemessene Zeit gerechnet. Eine Strafe ist positiv, eine + Gutschrift negativ. + {:else} + Gilt für alle {stageTimes.length} gewerteten Zeiten dieser Stage. Der + Wert wird gesetzt, nicht addiert — zweimal abschicken ergibt keine + doppelte Strafe. + {/if} + + + +
+
+ + +

+ 0 nimmt eine bestehende Korrektur wieder zurück. +

+
+ + {#if penaltyError} +

{penaltyError}

+ {/if} +
+ + + + + +
+