feat: Wer gedrueckt hat, und Strafzeiten je Zeit oder je Stage
startedBy und stoppedBy standen laengst im Datensatz und wurden bei jedem Start und Stopp geschrieben — nur sah sie niemand. Bei einer strittigen Messung ist das die erste Frage, also steht es jetzt unter dem Namen an der Zeit. Und die Korrektur: times.correction gab es, sie wird auf die Dauer gerechnet und im CSV exportiert, aber setzen konnte sie kein Mensch. Jetzt zwei Wege dorthin — an der einzelnen Zeit ueber die Spalte, die schon da war, und ueber alle gewerteten Zeiten der Stage auf einmal. Der Wert wird gesetzt und nicht addiert. Zweimal denselben Dialog abzuschicken darf keine doppelte Strafe ergeben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
This commit is contained in:
parent
bb56945fe3
commit
d01eae078a
1 changed files with 152 additions and 7 deletions
|
|
@ -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<string | null>(null)
|
||||
let penaltySeconds = $state(0)
|
||||
let penaltyBusy = $state(false)
|
||||
let penaltyError = $state<string | null>(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: laufende oben, darunter die Wertung -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Trophy class="h-4 w-4 text-primary" />
|
||||
Zeiten ({leaderboard.length} gewertet{running.length ? `, ${running.length} laufend` : ''})
|
||||
</CardTitle>
|
||||
{#if leaderboard.length > 0}
|
||||
<Button variant="outline" size="sm" onclick={() => openPenalty(null)}>
|
||||
<Pencil class="size-4 mr-2" />
|
||||
Strafzeit für alle
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="p-0">
|
||||
{#if rows.length === 0}
|
||||
|
|
@ -221,12 +292,44 @@
|
|||
{:else}
|
||||
—
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
Wer gedrueckt hat, steht an der
|
||||
Zeit. Bei einer strittigen Messung
|
||||
ist das die erste Frage, und die
|
||||
Daten dafuer lagen laengst im
|
||||
Datensatz — nur sah sie niemand.
|
||||
-->
|
||||
{#if userLabel(t.startedBy) || userLabel(t.stoppedBy)}
|
||||
<span class="block text-xs font-normal text-muted-foreground">
|
||||
{#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}
|
||||
</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right tabular-nums {live ? 'font-semibold' : ''}">
|
||||
{t.formattedTime}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right text-muted-foreground tabular-nums">
|
||||
{t.correction ? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)}s` : '–'}
|
||||
<Table.Cell class="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="tabular-nums font-normal {t.correction
|
||||
? ''
|
||||
: 'text-muted-foreground'}"
|
||||
tooltip="Zeitkorrektur"
|
||||
onclick={() => openPenalty(t.id, t.correction ?? 0)}
|
||||
>
|
||||
{t.correction
|
||||
? `${t.correction > 0 ? '+' : ''}${t.correction.toFixed(1)} s`
|
||||
: '–'}
|
||||
<Pencil class="size-3 ml-1 opacity-50" />
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
|
@ -273,3 +376,45 @@
|
|||
|
||||
<TimeDialog bind:open={timeDialog} eventId={event?.id ?? ''} stageId={stage?.id ?? null} />
|
||||
<ClockSyncDialog bind:open={clockDialog} />
|
||||
|
||||
<!-- Zeitkorrektur: eine Zahl in Sekunden, plus wie minus. -->
|
||||
<Dialog.Root bind:open={penaltyDialog}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{penaltyTime ? 'Zeitkorrektur' : 'Strafzeit für alle'}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{#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}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="penalty">Sekunden</Label>
|
||||
<Input id="penalty" type="number" step="0.1" bind:value={penaltySeconds} />
|
||||
<p class="text-xs text-muted-foreground">
|
||||
0 nimmt eine bestehende Korrektur wieder zurück.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if penaltyError}
|
||||
<p class="text-sm text-destructive">{penaltyError}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="ghost" onclick={() => (penaltyDialog = false)}>Abbrechen</Button>
|
||||
<Button onclick={savePenalty} disabled={penaltyBusy}>
|
||||
{penaltyBusy ? 'Wird gespeichert …' : 'Übernehmen'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
|
|
|||
Loading…
Reference in a new issue