feat: Grund zur Zeitkorrektur, "Strafzeit fuer alle" entfaellt

Das Schema traegt es bereits: times.comment gibt es seit dem ersten
Snapshot und wurde von der Anwendung nie benutzt. Der Grund steht jetzt
dort — optional, aber nach dem Event ist eine Strafe ohne Grund schwer
zu verteidigen. Er haengt im Tooltip an der Korrektur und steht als
eigene Spalte im CSV.

"Strafzeit fuer alle" ist raus. Eine Strafe trifft einen Fahrer; was
fuer alle gleich gilt, ist keine Strafe, sondern eine andere Stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
This commit is contained in:
Daniel Michelberger 2026-09-07 23:30:24 +02:00
parent 5c4d2fec4d
commit b80b1198fc
2 changed files with 42 additions and 39 deletions

View file

@ -228,8 +228,18 @@ export class TimeStore {
this.records = next
}
async applyCorrection(timeId: string, seconds: number) {
return await api.collection('times').update(timeId, { correction: seconds })
/**
* Zeitkorrektur setzen, mit optionalem Grund.
*
* Der Grund steht im vorhandenen Feld `comment`. Eine Strafe ohne Grund
* ist nach dem Event nicht mehr zu verteidigen Pflicht ist er trotzdem
* nicht: An der Strecke zaehlt, dass die Zahl stimmt.
*/
async applyCorrection(timeId: string, seconds: number, comment?: string) {
return await api.collection('times').update(timeId, {
correction: seconds,
comment: comment?.trim() ?? '',
})
}
async updateStatus(timeId: string, status: TimeStatus) {

View file

@ -56,8 +56,6 @@
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,
@ -97,38 +95,31 @@
// --- 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.
// wird auf die Dauer gerechnet; der Grund daneben im Feld `comment`.
// Immer an einer einzelnen Zeit — eine Strafe trifft einen Fahrer, und
// was fuer alle gleich gilt, ist keine Strafe, sondern eine andere Stage.
let penaltyDialog = $state(false)
let penaltyTime = $state<string | null>(null)
let penaltySeconds = $state(0)
let penaltyReason = $state('')
let penaltyBusy = $state(false)
let penaltyError = $state<string | null>(null)
function openPenalty(timeId: string | null, current = 0) {
function openPenalty(timeId: string, current = 0, reason = '') {
penaltyTime = timeId
penaltySeconds = current
penaltyReason = reason
penaltyError = null
penaltyDialog = true
}
async function savePenalty() {
if (penaltyBusy) return
if (penaltyBusy || !penaltyTime) 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)
}
}
await times.applyCorrection(penaltyTime, penaltySeconds, penaltyReason)
penaltyDialog = false
} catch (e: any) {
penaltyError = e.message ?? 'Die Korrektur konnte nicht gespeichert werden.'
@ -146,7 +137,7 @@
function exportCsv() {
if (leaderboard.length === 0) return
const rows = [['Platz', 'Nummer', 'Name', 'Zeit', 'Korrektur (s)']]
const rows = [['Platz', 'Nummer', 'Name', 'Zeit', 'Korrektur (s)', 'Grund']]
leaderboard.forEach((t, i) => {
const r = rider(t.rider)
rows.push([
@ -155,6 +146,7 @@
r ? riderName(r) : '',
t.formattedTime ?? '',
String(t.correction ?? 0),
t.comment ?? '',
])
})
@ -253,12 +245,6 @@
<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">
@ -379,10 +365,12 @@
class="tabular-nums font-normal {t.correction
? ''
: 'text-muted-foreground'}"
tooltip="Zeitkorrektur"
tooltip={t.comment
? `Zeitkorrektur: ${t.comment}`
: 'Zeitkorrektur'}
onclick={(e) => {
e.stopPropagation()
openPenalty(t.id, t.correction ?? 0)
openPenalty(t.id, t.correction ?? 0, t.comment ?? '')
}}
>
{t.correction
@ -446,18 +434,10 @@
<Dialog.Root bind:open={penaltyDialog}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>
{penaltyTime ? 'Zeitkorrektur' : 'Strafzeit für alle'}
</Dialog.Title>
<Dialog.Title>Zeitkorrektur</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>
@ -470,6 +450,19 @@
</p>
</div>
<div class="space-y-2">
<Label for="penalty-reason">Grund</Label>
<Input
id="penalty-reason"
bind:value={penaltyReason}
placeholder="z. B. Tor ausgelassen"
/>
<p class="text-xs text-muted-foreground">
Optional, aber nach dem Event ist eine Strafe ohne Grund schwer zu
verteidigen.
</p>
</div>
{#if penaltyError}
<p class="text-sm text-destructive">{penaltyError}</p>
{/if}